14 Commits

Author SHA1 Message Date
Alessandro Autiero
4327541ac6 Merge pull request #257 from Milxnor/master
Added dedicated_server endpoints & Update keychain
2025-03-23 16:33:39 +01:00
Gray
64dc971da4 Added dedicated_server endpoints 2025-03-22 07:47:46 -04:00
Gray
d36da909ed Update keychain (for events and new cosmetics) 2025-03-22 07:47:25 -04:00
Alessandro Autiero
90448eeaa1 10.0.7 2025-03-08 17:06:01 +01:00
Alessandro Autiero
b319479def 10.0.6 2025-02-04 13:50:01 +01:00
Alessandro Autiero
d5e41ed646 10.0.5 2024-12-30 19:13:08 +01:00
Alessandro Autiero
9e20ec86e6 Merge remote-tracking branch 'origin/master' 2024-12-29 21:43:04 +01:00
Alessandro Autiero
004fc41292 Dependency 2024-12-29 21:42:54 +01:00
Alessandro Autiero
ee466df630 Update README.md 2024-12-24 21:52:06 +01:00
Alessandro Autiero
fdb1d694d9 Better moving system 2024-12-10 17:18:10 +01:00
Alessandro Autiero
0cfa4af236 10.0.4 2024-12-10 14:45:56 +01:00
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
71 changed files with 2194 additions and 1931 deletions

View File

@@ -1,7 +1,7 @@
![Banner](https://i.imgur.com/p0P4tcI.png) ![Banner](https://i.imgur.com/p0P4tcI.png)
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/) GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
Join our discord at https://discord.gg/reboot Join our [Discord](https://discord.gg/rebootmp)
## Modules ## Modules

View File

@@ -1,8 +0,0 @@
# 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.

View File

@@ -1,98 +0,0 @@
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
}

View File

@@ -1,85 +0,0 @@
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"

File diff suppressed because it is too large Load Diff

View File

@@ -7932,6 +7932,35 @@ express.post("/fortnite/api/game/v2/profile/*/client/SetHeroCosmeticVariants", a
res.end(); res.end();
}); });
// any dedicated_server request
express.post("/fortnite/api/game/v2/profile/*/dedicated_server/*", async (req, res) => {
const profile = require(`./../profiles/${req.query.profileId || "athena"}.json`);
// do not change any of these or you will end up breaking it
var ApplyProfileChanges = [];
var BaseRevision = profile.rvn || 0;
var QueryRevision = req.query.rvn || -1;
// this doesn't work properly on version v12.20 and above but whatever
if (QueryRevision != BaseRevision) {
ApplyProfileChanges = [{
"changeType": "fullProfileUpdate",
"profile": profile
}];
}
res.json({
"profileRevision": profile.rvn || 0,
"profileId": req.query.profileId || "athena",
"profileChangesBaseRevision": BaseRevision,
"profileChanges": ApplyProfileChanges,
"profileCommandRevision": profile.commandRevision || 0,
"serverTime": new Date().toISOString(),
"responseVersion": 1
})
res.end();
});
// any mcp request that doesn't have something assigned to it // any mcp request that doesn't have something assigned to it
express.post("/fortnite/api/game/v2/profile/*/client/*", async (req, res) => { express.post("/fortnite/api/game/v2/profile/*/client/*", async (req, res) => {
const profile = require(`./../profiles/${req.query.profileId || "athena"}.json`); const profile = require(`./../profiles/${req.query.profileId || "athena"}.json`);

View File

@@ -1,87 +1,89 @@
import 'dart:io'; import 'dart:collection';
import 'package:args/args.dart'; class Parser {
import 'package:reboot_cli/src/game.dart'; final List<Command> commands;
import 'package:reboot_cli/src/reboot.dart';
import 'package:reboot_cli/src/server.dart';
import 'package:reboot_common/common.dart';
late String? username; Parser({required this.commands});
late bool host;
late bool verbose;
late String dll;
late FortniteVersion version;
late bool autoRestart;
void main(List<String> args) async { CommandCall? parse(List<String> args) {
stdout.writeln("Reboot Launcher"); var position = 0;
stdout.writeln("Wrote by Auties00"); var allowedCommands = _toMap(commands);
stdout.writeln("Version 1.0"); var allowedParameters = <String>{};
Command? command;
kill(); CommandCall? head;
CommandCall? tail;
var parser = ArgParser() String? parameterName;
..addOption("path", mandatory: true) while(position < args.length) {
..addOption("username") final current = args[position].toLowerCase();
..addOption("server-type", allowed: ServerType.values.map((entry) => entry.name), defaultsTo: ServerType.embedded.name) if(parameterName != null) {
..addOption("server-host") tail?.parameters[parameterName] = current;
..addOption("server-port") parameterName = null;
..addOption("matchmaking-address") }else if(allowedParameters.contains(current.toLowerCase())) {
..addOption("dll", defaultsTo: rebootDllFile.path) parameterName = current.substring(2);
..addFlag("update", defaultsTo: true, negatable: true) if(args.elementAtOrNull(position + 1) == '"') {
..addFlag("log", defaultsTo: false) position++;
..addFlag("host", defaultsTo: false) }
..addFlag("auto-restart", defaultsTo: false, negatable: true); }else {
var result = parser.parse(args); final newCommand = allowedCommands[current];
if(newCommand != null) {
dll = result["dll"]; final newCall = CommandCall(name: newCommand.name);
host = result["host"]; if(head == null) {
username = result["username"] ?? kDefaultPlayerName; head = newCall;
verbose = result["log"]; tail = newCall;
version = FortniteVersion(name: "Dummy", location: Directory(result["path"])); }
if(tail != null) {
await downloadRequiredDLLs(); tail.subCall = newCall;
if(result["update"]) { }
stdout.writeln("Updating reboot dll..."); tail = newCall;
try { command = newCommand;
await downloadRebootDll(kRebootDownloadUrl); allowedCommands = _toMap(newCommand.subCommands);
}catch(error){ allowedParameters = _toParameters(command);
stderr.writeln("Cannot update reboot dll: $error");
} }
} }
position++;
stdout.writeln("Launching game..."); }
var executable = version.shippingExecutable; return head;
if(executable == null){
throw Exception("Missing game executable at: ${version.location.path}");
} }
final serverHost = result["server-host"]?.trim(); Set<String> _toParameters(Command? parent) => parent?.parameters
if(serverHost?.isEmpty == true){ .map((e) => '--${e.toLowerCase()}')
throw Exception("Missing host name"); .toSet() ?? {};
}
final serverPort = result["server-port"]?.trim(); Map<String, Command> _toMap(List<Command> children) => Map.fromIterable(
if(serverPort?.isEmpty == true){ children,
throw Exception("Missing port"); key: (command) => command.name.toLowerCase(),
} value: (command) => command
final serverPortNumber = serverPort == null ? null : int.tryParse(serverPort);
if(serverPort != null && serverPortNumber == null){
throw Exception("Invalid port, use only numbers");
}
var started = await startServerCli(
serverHost,
serverPortNumber,
ServerType.values.firstWhere((element) => element.name == result["server-type"])
); );
if(!started){
stderr.writeln("Cannot start server!");
return;
} }
writeMatchmakingIp(result["matchmaking-address"]); class Command {
autoRestart = result["auto-restart"]; final String name;
await startGame(); final List<String> parameters;
final List<Command> subCommands;
const Command({required this.name, required this.parameters, required this.subCommands});
@override
String toString() => 'Command{name: $name, parameters: $parameters, subCommands: $subCommands}';
}
class Parameter {
final String name;
final bool Function(String) validator;
const Parameter({required this.name, required this.validator});
@override
String toString() => 'Parameter{name: $name, validator: $validator}';
}
class CommandCall {
final String name;
final Map<String, String> parameters;
CommandCall? subCall;
CommandCall({required this.name}) : parameters = {};
@override
String toString() => 'CommandCall{name: $name, parameters: $parameters, subCall: $subCall}';
} }

102
cli/lib/main.dart Normal file
View File

@@ -0,0 +1,102 @@
import 'package:interact/interact.dart';
import 'package:reboot_cli/cli.dart';
import 'package:tint/tint.dart';
const Command _buildImport = Command(name: 'import', parameters: ['version', 'path'], subCommands: []);
const Command _buildDownload = Command(name: 'download', parameters: ['version', 'path'], subCommands: []);
const Command _build = Command(name: 'build', parameters: [], subCommands: [_buildImport, _buildDownload]);
const Command _play = Command(name: 'play', parameters: [], subCommands: []);
const Command _host = Command(name: 'host', parameters: [], subCommands: []);
const Command _backend = Command(name: 'backend', parameters: [], subCommands: []);
void main(List<String> args) {
_welcome();
final parser = Parser(commands: [_build, _play, _host, _backend]);
final command = parser.parse(args);
print(command);
_handleRootCommand(command);
}
void _handleRootCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
case 'build':
_handleBuildCommand(call?.subCall);
break;
case 'play':
_handleBuildCommand(call?.subCall);
break;
case 'host':
_handleBuildCommand(call?.subCall);
break;
case 'backend':
_handleBuildCommand(call?.subCall);
break;
default:
_askRootCommand();
break;
}
}
void _askRootCommand() {
final commands = [_build.name, _play.name, _host.name, _backend.name];
final commandSelector = Select.withTheme(
prompt: ' Select a command:',
options: commands,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '')
);
_handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
}
void _handleBuildCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
case 'import':
_handleBuildImportCommand(call!);
break;
case 'download':
_handleBuildDownloadCommand(call!);
break;
default:
_askBuildCommand();
break;
}
}
void _handleBuildImportCommand(CommandCall call) {
final version = call.parameters['path'];
final path = call.parameters['path'];
print(version);
print(path);
}
void _handleBuildDownloadCommand(CommandCall call) {
}
void _askBuildCommand() {
final commands = [_buildImport.name, _buildDownload.name];
final commandSelector = Select.withTheme(
prompt: ' Select a build command:',
options: commands,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '')
);
_handleBuildCommand(CommandCall(name: commands[commandSelector.interact()]));
}
void _handlePlayCommand(CommandCall? call) {
}
void _handleHostCommand(CommandCall? call) {
}
void _handleBackendCommand(CommandCall? call) {
}
void _welcome() => print("""
🎮 Reboot Launcher
🔥 Launch, manage, and play Fortnite using Project Reboot!
🚀 Developed by Auties00 - Version 10.0.7
""".green());

View File

@@ -1,123 +0,0 @@
import 'dart:io';
import 'package:process_run/process_run.dart';
import 'package:reboot_cli/cli.dart';
import 'package:reboot_common/common.dart';
Process? _gameProcess;
Process? _launcherProcess;
Process? _eacProcess;
Future<void> startGame() async {
await _startLauncherProcess(version);
await _startEacProcess(version);
var executable = await version.shippingExecutable;
if (executable == null) {
throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?");
}
if (username == null) {
username = "Reboot${host ? 'Host' : 'Player'}";
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
}
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
..exitCode.then((_) => _onClose())
..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
_injectOrShowError("cobalt.dll");
}
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.launcherExecutable == null) {
return;
}
_launcherProcess = await Process.start(dummyVersion.launcherExecutable!.path, []);
suspend(_launcherProcess!.pid);
}
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.eacExecutable == null) {
return;
}
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
suspend(_eacProcess!.pid);
}
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
if(verbose) {
stdout.writeln(line);
}
handleGameOutput(
line: line,
host: hosting,
onDisplayAttached: () {}, // TODO: Support virtual desktops
onLoggedIn: onLoggedIn,
onMatchEnd: onMatchEnd,
onShutdown: onShutdown,
onTokenError: onTokenError,
onBuildCorrupted: onBuildCorrupted
);
if (line.contains(kShutdownLine)) {
_onClose();
return;
}
if(kCannotConnectErrors.any((element) => line.contains(element))){
stderr.writeln("The backend doesn't work! Token expired");
_onClose();
return;
}
if(line.contains("Region ")){
if(hosting) {
_injectOrShowError(dll, false);
}else {
_injectOrShowError("console.dll");
}
_injectOrShowError("memory.dll");
}
}
void _kill() {
_gameProcess?.kill(ProcessSignal.sigabrt);
_launcherProcess?.kill(ProcessSignal.sigabrt);
_eacProcess?.kill(ProcessSignal.sigabrt);
}
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
if (_gameProcess == null) {
return;
}
try {
stdout.writeln("Injecting $binary...");
var dll = locate ? File("${dllsDirectory.path}\\$binary") : File(binary);
if(!dll.existsSync()){
throw Exception("Cannot inject $dll: missing file");
}
await injectDll(_gameProcess!.pid, dll);
} catch (exception) {
throw Exception("Cannot inject binary: $binary");
}
}
void _onClose() {
_kill();
sleep(const Duration(seconds: 3));
stdout.writeln("The game was closed");
if(autoRestart){
stdout.writeln("Restarting automatically game");
startGame();
return;
}
exit(0);
}

View File

@@ -1,55 +0,0 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:reboot_common/common.dart';
// TODO: Use github
const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memory.dll";
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
Future<void> downloadRequiredDLLs() async {
stdout.writeln("Downloading necessary components...");
var consoleDll = File("${dllsDirectory.path}\\console.dll");
if(!consoleDll.existsSync()){
var response = await http.get(Uri.parse(_consoleDownload));
if(response.statusCode != 200){
throw Exception("Cannot download console.dll");
}
await consoleDll.writeAsBytes(response.bodyBytes);
}
var craniumDll = File("${dllsDirectory.path}\\cobalt.dll");
if(!craniumDll.existsSync()){
var response = await http.get(Uri.parse(_baseDownload));
if(response.statusCode != 200){
throw Exception("Cannot download cobalt.dll");
}
await craniumDll.writeAsBytes(response.bodyBytes);
}
var memoryFixDll = File("${dllsDirectory.path}\\memory.dll");
if(!memoryFixDll.existsSync()){
var response = await http.get(Uri.parse(_memoryFixDownload));
if(response.statusCode != 200){
throw Exception("Cannot download memory.dll");
}
await memoryFixDll.writeAsBytes(response.bodyBytes);
}
if(!backendDirectory.existsSync()){
var response = await http.get(Uri.parse(_embeddedConfigDownload));
if(response.statusCode != 200){
throw Exception("Cannot download embedded server config");
}
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, backendDirectory.path);
}
}

View File

@@ -1,60 +0,0 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/util/backend.dart' as server;
Future<bool> startServerCli(String? host, int? port, ServerType type) async {
stdout.writeln("Starting backend server...");
switch(type){
case ServerType.local:
final result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
if(result == null){
throw Exception("Local backend server is not running");
}
stdout.writeln("Detected local backend server");
return true;
case ServerType.embedded:
stdout.writeln("Starting an embedded server...");
await server.startEmbeddedBackend(false);
var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
if(result == null){
throw Exception("Cannot start embedded server");
}
return true;
case ServerType.remote:
if(host == null){
throw Exception("Missing host for remote server");
}
if(port == null){
throw Exception("Missing host for remote server");
}
stdout.writeln("Starting a reverse proxy to $host:$port");
return await _changeReverseProxyState(host, port) != null;
}
}
Future<HttpServer?> _changeReverseProxyState(String host, int port) async {
try{
var uri = await pingBackend(host, port);
if(uri == null){
return null;
}
return await server.startRemoteBackendProxy(uri);
}catch(error){
throw Exception("Cannot start reverse proxy");
}
}
void kill() async {
try {
await Process.run("taskkill", ["/f", "/im", "FortniteLauncher.exe"]);
await Process.run("taskkill", ["/f", "/im", "FortniteClient-Win64-Shipping_EAC.exe"]);
}catch(_){
}
}

View File

@@ -1,17 +1,18 @@
name: reboot_cli name: reboot_cli
description: Command Line Interface for Project Reboot description: Command Line Interface for Project Reboot
version: "1.0.0" version: "10.0.7"
publish_to: 'none' publish_to: 'none'
environment: environment:
sdk: ">=2.19.0 <=3.3.4" sdk: ">=2.19.0 <=3.5.3"
dependencies: dependencies:
reboot_common: reboot_common:
path: ./../common path: ./../common
args: ^2.3.1 tint: ^2.0.1
process_run: ^0.13.1 interact: ^2.2.0
args: ^2.6.0
dependency_overrides: dependency_overrides:
xml: ^6.3.0 xml: ^6.3.0

View File

@@ -22,4 +22,5 @@ const List<String> kCannotConnectErrors = [
"UOnlineAccountCommon::ForceLogout" "UOnlineAccountCommon::ForceLogout"
]; ];
const String kGameFinishedLine = "PlayersLeft: 1"; const String kGameFinishedLine = "PlayersLeft: 1";
const String kDisplayInitializedLine = "Display"; const String kDisplayLine = "Display";
const String kDisplayInitializedLine = "Initialized";

View File

@@ -1,9 +1,10 @@
enum InjectableDll { enum InjectableDll {
console, console,
starfall, auth,
reboot, gameServer,
memoryLeak
} }
extension InjectableDllVersionAware on InjectableDll { extension InjectableDllVersionAware on InjectableDll {
bool get isVersionDependent => this == InjectableDll.reboot; bool get isVersionDependent => this == InjectableDll.gameServer;
} }

View File

@@ -1,9 +1,12 @@
import 'dart:io';
class ServerResult { class ServerResult {
final ServerResultType type; final ServerResultType type;
final ServerImplementation? implementation;
final Object? error; final Object? error;
final StackTrace? stackTrace; final StackTrace? stackTrace;
ServerResult(this.type, {this.error, this.stackTrace}); ServerResult(this.type, {this.implementation, this.error, this.stackTrace});
@override @override
String toString() { String toString() {
@@ -11,22 +14,32 @@ class ServerResult {
} }
} }
class ServerImplementation {
final Process? process;
final HttpServer? server;
ServerImplementation({this.process, this.server});
}
enum ServerResultType { enum ServerResultType {
starting, starting,
startMissingHostError,
startMissingPortError,
startIllegalPortError,
startFreeingPort,
startFreePortSuccess,
startFreePortError,
startPingingRemote,
startPingingLocal,
startPingError,
startedImplementation,
startSuccess, startSuccess,
startError, startError,
stopping, stopping,
stopSuccess, stopSuccess,
stopError, stopError;
missingHostError,
missingPortError, bool get isStart => name.contains("start");
illegalPortError,
freeingPort,
freePortSuccess,
freePortError,
pingingRemote,
pingingLocal,
pingError;
bool get isError => name.contains("Error"); bool get isError => name.contains("Error");

View File

@@ -15,6 +15,122 @@ final Semaphore _semaphore = Semaphore();
String? _lastIp; String? _lastIp;
String? _lastPort; String? _lastPort;
Stream<ServerResult> startBackend({required ServerType type, required String host, required String port, required bool detached, required void Function(String) onError}) async* {
Process? process;
HttpServer? server;
try {
host = host.trim();
port = port.trim();
if(type != ServerType.local || port != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting);
}
if (host.isEmpty) {
yield ServerResult(ServerResultType.startMissingHostError);
return;
}
if (port.isEmpty) {
yield ServerResult(ServerResultType.startMissingPortError);
return;
}
final portNumber = int.tryParse(port);
if (portNumber == null) {
yield ServerResult(ServerResultType.startIllegalPortError);
return;
}
if ((type != ServerType.local || port != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
yield ServerResult(ServerResultType.startFreeingPort);
final result = await freeBackendPort();
if(!result) {
yield ServerResult(ServerResultType.startFreePortError);
return;
}
yield ServerResult(ServerResultType.startFreePortSuccess);
}
switch(type){
case ServerType.embedded:
process = await startEmbeddedBackend(detached, onError: onError);
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(process: process));
break;
case ServerType.remote:
yield ServerResult(ServerResultType.startPingingRemote);
final uriResult = await pingBackend(host, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.startPingError);
return;
}
server = await startRemoteBackendProxy(uriResult);
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
break;
case ServerType.local:
if(portNumber != kDefaultBackendPort) {
yield ServerResult(ServerResultType.startPingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.startPingError);
return;
}
server = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$port"));
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
}
break;
}
yield ServerResult(ServerResultType.startPingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
if(uriResult == null) {
yield ServerResult(ServerResultType.startPingError);
process?.kill(ProcessSignal.sigterm);
server?.close(force: true);
return;
}
yield ServerResult(ServerResultType.startSuccess);
}catch(error, stackTrace) {
yield ServerResult(
ServerResultType.startError,
error: error,
stackTrace: stackTrace
);
process?.kill(ProcessSignal.sigterm);
server?.close(force: true);
}
}
Stream<ServerResult> stopBackend({required ServerType type, required ServerImplementation? implementation}) async* {
yield ServerResult(ServerResultType.stopping);
try{
switch(type){
case ServerType.embedded:
final process = implementation?.process;
if(process != null) {
Process.killPid(process.pid, ProcessSignal.sigterm);
}
break;
case ServerType.remote:
await implementation?.server?.close(force: true);
break;
case ServerType.local:
await implementation?.server?.close(force: true);
break;
}
yield ServerResult(ServerResultType.stopSuccess);
}catch(error, stackTrace){
yield ServerResult(
ServerResultType.stopError,
error: error,
stackTrace: stackTrace
);
}
}
Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async { Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
final process = await startProcess( final process = await startProcess(
executable: backendStartExecutable, executable: backendStartExecutable,
@@ -25,7 +141,9 @@ Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onEr
log("[BACKEND] Error: $error"); log("[BACKEND] Error: $error");
onError?.call(error); onError?.call(error);
}); });
if(!detached) {
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
}
return process; return process;
} }

View File

@@ -13,37 +13,99 @@ import 'package:http/http.dart' as http;
const String kStopBuildDownloadSignal = "kill"; const String kStopBuildDownloadSignal = "kill";
final Uri _archiveSourceUrl = Uri.parse("https://builds.rebootfn.org/versions.json");
final int _ariaPort = 6800; final int _ariaPort = 6800;
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc'); final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10); final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
final String _ariaSecret = "RebootLauncher"; final String _ariaSecret = "RebootLauncher";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
final List<FortniteBuild> downloadableBuilds = [
FortniteBuild(version: Version.parse("1.7.2"), link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
FortniteBuild(version: Version.parse("1.8"), link: "https://public.simplyblk.xyz/1.8.rar", available: true),
FortniteBuild(version: Version.parse("1.8.1"), link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
FortniteBuild(version: Version.parse("1.8.2"), link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
FortniteBuild(version: Version.parse("1.9"), link: "https://public.simplyblk.xyz/1.9.rar", available: true),
FortniteBuild(version: Version.parse("1.9.1"), link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
FortniteBuild(version: Version.parse("1.10"), link: "https://public.simplyblk.xyz/1.10.rar", available: true),
FortniteBuild(version: Version.parse("1.11"), link: "https://public.simplyblk.xyz/1.11.zip", available: true),
FortniteBuild(version: Version.parse("2.1.0"), link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
FortniteBuild(version: Version.parse("2.2.0"), link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
FortniteBuild(version: Version.parse("2.3"), link: "https://public.simplyblk.xyz/2.3.rar", available: true),
FortniteBuild(version: Version.parse("2.4.0"), link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
FortniteBuild(version: Version.parse("2.4.2"), link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
FortniteBuild(version: Version.parse("2.5.0"), link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
FortniteBuild(version: Version.parse("3.0"), link: "https://public.simplyblk.xyz/3.0.zip", available: true),
FortniteBuild(version: Version.parse("3.1"), link: "https://public.simplyblk.xyz/3.1.rar", available: true),
FortniteBuild(version: Version.parse("3.1.1"), link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
FortniteBuild(version: Version.parse("3.2"), link: "https://public.simplyblk.xyz/3.2.zip", available: true),
FortniteBuild(version: Version.parse("3.3"), link: "https://public.simplyblk.xyz/3.3.rar", available: true),
FortniteBuild(version: Version.parse("3.5"), link: "https://public.simplyblk.xyz/3.5.rar", available: true),
FortniteBuild(version: Version.parse("3.6"), link: "https://public.simplyblk.xyz/3.6.zip", available: true),
FortniteBuild(version: Version.parse("4.0"), link: "https://public.simplyblk.xyz/4.0.zip", available: true),
FortniteBuild(version: Version.parse("4.1"), link: "https://public.simplyblk.xyz/4.1.zip", available: true),
FortniteBuild(version: Version.parse("4.2"), link: "https://public.simplyblk.xyz/4.2.zip", available: true),
FortniteBuild(version: Version.parse("4.4"), link: "https://public.simplyblk.xyz/4.4.rar", available: true),
FortniteBuild(version: Version.parse("4.5"), link: "https://public.simplyblk.xyz/4.5.rar", available: true),
FortniteBuild(version: Version.parse("5.00"), link: "https://public.simplyblk.xyz/5.00.rar", available: true),
FortniteBuild(version: Version.parse("5.0.1"), link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
FortniteBuild(version: Version.parse("5.10"), link: "https://public.simplyblk.xyz/5.10.rar", available: true),
FortniteBuild(version: Version.parse("5.21"), link: "https://public.simplyblk.xyz/5.21.rar", available: true),
FortniteBuild(version: Version.parse("5.30"), link: "https://public.simplyblk.xyz/5.30.rar", available: true),
FortniteBuild(version: Version.parse("5.40"), link: "https://public.simplyblk.xyz/5.40.rar", available: true),
FortniteBuild(version: Version.parse("6.00"), link: "https://public.simplyblk.xyz/6.00.rar", available: true),
FortniteBuild(version: Version.parse("6.01"), link: "https://public.simplyblk.xyz/6.01.rar", available: true),
FortniteBuild(version: Version.parse("6.1.1"), link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
FortniteBuild(version: Version.parse("6.02"), link: "https://public.simplyblk.xyz/6.02.rar", available: true),
FortniteBuild(version: Version.parse("6.2.1"), link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10"), link: "https://public.simplyblk.xyz/6.10.rar", available: true),
FortniteBuild(version: Version.parse("6.10.1"), link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10.2"), link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
FortniteBuild(version: Version.parse("6.21"), link: "https://public.simplyblk.xyz/6.21.rar", available: true),
FortniteBuild(version: Version.parse("6.22"), link: "https://public.simplyblk.xyz/6.22.rar", available: true),
FortniteBuild(version: Version.parse("6.30"), link: "https://public.simplyblk.xyz/6.30.rar", available: true),
FortniteBuild(version: Version.parse("6.31"), link: "https://public.simplyblk.xyz/6.31.rar", available: true),
FortniteBuild(version: Version.parse("7.00"), link: "https://public.simplyblk.xyz/7.00.rar", available: true),
FortniteBuild(version: Version.parse("7.10"), link: "https://public.simplyblk.xyz/7.10.rar", available: true),
FortniteBuild(version: Version.parse("7.20"), link: "https://public.simplyblk.xyz/7.20.rar", available: true),
FortniteBuild(version: Version.parse("7.30"), link: "https://public.simplyblk.xyz/7.30.zip", available: true),
FortniteBuild(version: Version.parse("7.40"), link: "https://public.simplyblk.xyz/7.40.rar", available: true),
FortniteBuild(version: Version.parse("8.00"), link: "https://public.simplyblk.xyz/8.00.zip", available: true),
FortniteBuild(version: Version.parse("8.20"), link: "https://public.simplyblk.xyz/8.20.rar", available: true),
FortniteBuild(version: Version.parse("8.30"), link: "https://public.simplyblk.xyz/8.30.rar", available: true),
FortniteBuild(version: Version.parse("8.40"), link: "https://public.simplyblk.xyz/8.40.zip", available: true),
FortniteBuild(version: Version.parse("8.50"), link: "https://public.simplyblk.xyz/8.50.zip", available: true),
FortniteBuild(version: Version.parse("8.51"), link: "https://public.simplyblk.xyz/8.51.rar", available: true),
FortniteBuild(version: Version.parse("9.00"), link: "https://public.simplyblk.xyz/9.00.zip", available: true),
FortniteBuild(version: Version.parse("9.01"), link: "https://public.simplyblk.xyz/9.01.zip", available: true),
FortniteBuild(version: Version.parse("9.10"), link: "https://public.simplyblk.xyz/9.10.rar", available: true),
FortniteBuild(version: Version.parse("9.21"), link: "https://public.simplyblk.xyz/9.21.zip", available: true),
FortniteBuild(version: Version.parse("9.30"), link: "https://public.simplyblk.xyz/9.30.zip", available: true),
FortniteBuild(version: Version.parse("9.40"), link: "https://public.simplyblk.xyz/9.40.zip", available: true),
FortniteBuild(version: Version.parse("9.41"), link: "https://public.simplyblk.xyz/9.41.rar", available: true),
FortniteBuild(version: Version.parse("10.00"), link: "https://public.simplyblk.xyz/10.00.zip", available: true),
FortniteBuild(version: Version.parse("10.10"), link: "https://public.simplyblk.xyz/10.10.zip", available: true),
FortniteBuild(version: Version.parse("10.20"), link: "https://public.simplyblk.xyz/10.20.zip", available: true),
FortniteBuild(version: Version.parse("10.31"), link: "https://public.simplyblk.xyz/10.31.zip", available: true),
FortniteBuild(version: Version.parse("10.40"), link: "https://public.simplyblk.xyz/10.40.rar", available: true),
FortniteBuild(version: Version.parse("11.00"), link: "https://public.simplyblk.xyz/11.00.zip", available: true),
FortniteBuild(version: Version.parse("11.31"), link: "https://public.simplyblk.xyz/11.31.rar", available: true),
FortniteBuild(version: Version.parse("12.00"), link: "https://public.simplyblk.xyz/12.00.rar", available: true),
FortniteBuild(version: Version.parse("12.21"), link: "https://public.simplyblk.xyz/12.21.zip", available: true),
FortniteBuild(version: Version.parse("12.50"), link: "https://public.simplyblk.xyz/12.50.zip", available: true),
FortniteBuild(version: Version.parse("12.61"), link: "https://public.simplyblk.xyz/12.61.zip", available: true),
FortniteBuild(version: Version.parse("13.00"), link: "https://public.simplyblk.xyz/13.00.rar", available: true),
FortniteBuild(version: Version.parse("13.40"), link: "https://public.simplyblk.xyz/13.40.zip", available: true),
FortniteBuild(version: Version.parse("14.00"), link: "https://public.simplyblk.xyz/14.00.rar", available: true),
FortniteBuild(version: Version.parse("14.40"), link: "https://public.simplyblk.xyz/14.40.rar", available: true),
FortniteBuild(version: Version.parse("14.60"), link: "https://public.simplyblk.xyz/14.60.rar", available: true),
FortniteBuild(version: Version.parse("15.30"), link: "https://public.simplyblk.xyz/15.30.rar", available: true),
FortniteBuild(version: Version.parse("16.40"), link: "https://public.simplyblk.xyz/16.40.rar", available: true),
FortniteBuild(version: Version.parse("17.30"), link: "https://public.simplyblk.xyz/17.30.zip", available: true),
FortniteBuild(version: Version.parse("17.50"), link: "https://public.simplyblk.xyz/17.50.zip", available: true),
FortniteBuild(version: Version.parse("18.40"), link: "https://public.simplyblk.xyz/18.40.zip", available: true),
FortniteBuild(version: Version.parse("19.10"), link: "https://public.simplyblk.xyz/19.10.rar", available: true),
FortniteBuild(version: Version.parse("20.40"), link: "https://public.simplyblk.xyz/20.40.zip", available: true),
];
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
final response = await http.get(_archiveSourceUrl);
if (response.statusCode != 200) {
return [];
}
return jsonDecode(response.body)
.map((entry) {
try {
final fileUrl = entry as String;
final fileName = Uri.parse(fileUrl).pathSegments.last;
final fileNameWithoutExtension = path.basenameWithoutExtension(fileName);
return FortniteBuild(
version: Version.parse(fileNameWithoutExtension),
link: entry,
available: true
);
}catch(_) {
return null;
}
})
.whereType<FortniteBuild>()
.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 fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
@@ -134,17 +196,13 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
} }
Future<void> _startAriaServer() async { Future<void> _startAriaServer() async {
final running = await _isAriaRunning(); await stopDownloadServer();
if(running) {
await killProcessByPort(_ariaPort);
}
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe"); final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) { if(!aria2c.existsSync()) {
throw "Missing aria2c.exe"; throw "Missing aria2c.exe";
} }
await startProcess( final process = await startProcess(
executable: aria2c, executable: aria2c,
args: [ args: [
"--max-connection-per-server=${Platform.numberOfProcessors}", "--max-connection-per-server=${Platform.numberOfProcessors}",
@@ -153,10 +211,15 @@ Future<void> _startAriaServer() async {
"--rpc-listen-all=true", "--rpc-listen-all=true",
"--rpc-allow-origin-all", "--rpc-allow-origin-all",
"--rpc-secret=$_ariaSecret", "--rpc-secret=$_ariaSecret",
"--rpc-listen-port=$_ariaPort" "--rpc-listen-port=$_ariaPort",
"--file-allocation=none",
"--check-certificate=false"
], ],
window: false 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++) { for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) { if(await _isAriaRunning()) {
return; return;
@@ -177,8 +240,8 @@ Future<bool> _isAriaRunning() async {
"token:${_ariaSecret}" "token:${_ariaSecret}"
] ]
}; };
await http.post(_ariaEndpoint, body: jsonEncode(statusRequest)); final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return true; return response.statusCode == 200;
}catch(_) { }catch(_) {
return false; return false;
} }
@@ -227,11 +290,16 @@ Future<void> _stopAriaDownload(String downloadId) async {
] ]
}; };
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest)); await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) { }catch(error) {
throw "Stop failed (${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 {
Process? process; Process? process;

View File

@@ -5,14 +5,19 @@ import 'package:http/http.dart' as http;
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';
bool _watcher = false;
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll"); final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll"); final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
const String kRebootBelowS20DownloadUrl = const String kRebootBelowS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip"; "https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
const String kRebootAboveS20DownloadUrl = const String kRebootAboveS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip"; "https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
const String _kRebootBelowS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootFallback.zip";
const String _kRebootAboveS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootS20Fallback.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 rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists(); final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
@@ -20,7 +25,22 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours); return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
} }
Future<void> downloadCriticalDll(String name, String outputPath) async { Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
String? name;
switch(dll) {
case InjectableDll.console:
name = "console.dll";
case InjectableDll.auth:
name = "cobalt.dll";
case InjectableDll.memoryLeak:
name = "memory.dll";
case InjectableDll.gameServer:
name = null;
}
if(name == null) {
return;
}
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name")); final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
if(response.statusCode != 200) { if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}"); throw Exception("Cannot download $name: status code ${response.statusCode}");
@@ -31,12 +51,15 @@ Future<void> downloadCriticalDll(String name, String outputPath) async {
await output.writeAsBytes(response.bodyBytes, flush: true); await output.writeAsBytes(response.bodyBytes, flush: true);
} }
Future<void> downloadRebootDll(File file, String url) async { Future<void> downloadRebootDll(File file, String url, bool aboveS20) async {
Directory? outputDir; Directory? outputDir;
try { try {
final response = await http.get(Uri.parse(url)); var response = await http.get(Uri.parse(url));
if(response.statusCode != 200) { if(response.statusCode != 200) {
throw Exception("Cannot download reboot.zip: status code ${response.statusCode}"); response = await http.get(Uri.parse(aboveS20 ? _kRebootAboveS20FallbackDownloadUrl : _kRebootBelowS20FallbackDownloadUrl));
if(response.statusCode != 200) {
throw Exception("status code ${response.statusCode}");
}
} }
outputDir = await installationDirectory.createTemp("reboot_out"); outputDir = await installationDirectory.createTemp("reboot_out");
@@ -57,16 +80,3 @@ Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null; : null;
} }
Stream<String> watchDlls() async* {
if(_watcher) {
return;
}
_watcher = true;
await for(final event in dllsDirectory.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (event.path.endsWith(".dll")) {
yield event.path;
}
}
}

View File

@@ -168,20 +168,6 @@ bool resume(int pid) {
} }
} }
Future<void> watchProcess(int pid) => Isolate.run(() {
final processHandle = OpenProcess(FILE_ACCESS_RIGHTS.SYNCHRONIZE, FALSE, pid);
if (processHandle == 0) {
return;
}
try {
WaitForSingleObject(processHandle, INFINITE);
}finally {
CloseHandle(processHandle);
}
});
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) { List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
log("[PROCESS] Generating reboot args"); log("[PROCESS] Generating reboot args");
if(password.isEmpty) { if(password.isEmpty) {
@@ -264,17 +250,13 @@ void handleGameOutput({
}else if(line.contains(kGameFinishedLine) && host) { }else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line"); log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd(); onMatchEnd();
}else if(line.contains(kDisplayInitializedLine) && host) { }else if(line.contains(kDisplayLine) && line.contains(kDisplayInitializedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line"); log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
onDisplayAttached(); onDisplayAttached();
} }
} }
String _parseUsername(String username, bool host) { String _parseUsername(String username, bool host) {
if(host) {
return "Player${Random().nextInt(1000)}";
}
if (username.isEmpty) { if (username.isEmpty) {
return kDefaultPlayerName; return kDefaultPlayerName;
} }
@@ -296,16 +278,8 @@ final class _ExtendedProcess implements Process {
_stdout = attached ? delegate.stdout.asBroadcastStream() : null, _stdout = attached ? delegate.stdout.asBroadcastStream() : null,
_stderr = attached ? delegate.stderr.asBroadcastStream() : null; _stderr = attached ? delegate.stderr.asBroadcastStream() : null;
@override @override
Future<int> get exitCode { Future<int> get exitCode => _delegate.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);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -128,7 +128,7 @@
"importVersionDescription": "Import a new version of Fortnite into the launcher", "importVersionDescription": "Import a new version of Fortnite into the launcher",
"addLocalBuildName": "Add a version from this PC's local storage", "addLocalBuildName": "Add a version from this PC's local storage",
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work", "addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
"addVersion": "Add version", "addVersion": "New version",
"downloadBuildName": "Download any version from the cloud", "downloadBuildName": "Download any version from the cloud",
"downloadBuildDescription": "Download any Fortnite build easily from the cloud", "downloadBuildDescription": "Download any Fortnite build easily from the cloud",
"downloadBuildContent": "Download build", "downloadBuildContent": "Download build",
@@ -216,7 +216,6 @@
"downloadedVersion": "The download was completed successfully!", "downloadedVersion": "The download was completed successfully!",
"download": "Download", "download": "Download",
"downloading": "Downloading...", "downloading": "Downloading...",
"allocatingSpace": "Allocating disk space...",
"startingDownload": "Starting download...", "startingDownload": "Starting download...",
"extracting": "Extracting...", "extracting": "Extracting...",
"buildProgress": "{progress}%", "buildProgress": "{progress}%",
@@ -237,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",

View File

@@ -15,8 +15,8 @@ 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/widget/message/error.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/widget/page/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:reboot_launcher/src/util/url_protocol.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
@@ -82,9 +82,7 @@ Future<void> _startApp() async {
errors.add(uncaughtError); errors.add(uncaughtError);
} finally{ } finally{
log("[APP] Started applications with errors: $errors"); log("[APP] Started applications with errors: $errors");
runApp(RebootApplication( runApp(RebootApplication(errors: errors));
errors: errors,
));
} }
} }
@@ -171,11 +169,12 @@ Future<void> _initWindow() async {
}else { }else {
await windowManager.setAlignment(Alignment.center); await windowManager.setAlignment(Alignment.center);
} }
await windowManager.setPreventClose(true);
await windowManager.setResizable(true);
if(isWin11) { if(isWin11) {
await Window.setEffect( await Window.setEffect(
effect: WindowEffect.acrylic, effect: WindowEffect.acrylic,
color: Colors.transparent, color: Colors.green,
dark: isDarkMode dark: isDarkMode
); );
} }
@@ -231,7 +230,6 @@ Future<List<Object>> _initStorage() async {
errors.add(error); errors.add(error);
} }
return errors; return errors;
} }
@@ -253,7 +251,11 @@ class _RebootApplicationState extends State<RebootApplication> {
} }
void _handleErrors(List<Object?> errors) { void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false)); for(final error in errors) {
if(error != null) {
onError(error, null, false);
}
}
} }
@override @override

View File

@@ -1,13 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.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/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/keyboard.dart'; import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
class BackendController extends GetxController { class BackendController extends GetxController {
static const String storageName = "v2_backend_storage"; static const String storageName = "v2_backend_storage";
@@ -22,10 +34,9 @@ class BackendController extends GetxController {
late final Rx<PhysicalKeyboardKey> consoleKey; late final Rx<PhysicalKeyboardKey> consoleKey;
late final RxBool started; late final RxBool started;
late final RxBool detached; late final RxBool detached;
StreamSubscription? worker; late final List<InfoBarEntry> _infoBars;
int? embeddedProcessPid; StreamSubscription? _worker;
HttpServer? localServer; ServerImplementation? _implementation;
HttpServer? remoteServer;
BackendController() { BackendController() {
_storage = appWithNoStorage ? null : GetStorage(storageName); _storage = appWithNoStorage ? null : GetStorage(storageName);
@@ -35,11 +46,6 @@ class BackendController extends GetxController {
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) {
return;
}
stop();
}); });
host = TextEditingController(text: _readHost()); host = TextEditingController(text: _readHost());
host.addListener(() => host.addListener(() =>
@@ -70,15 +76,7 @@ class BackendController extends GetxController {
} }
}); });
gameServerAddressFocusNode = FocusNode(); gameServerAddressFocusNode = FocusNode();
consoleKey = Rx(_readConsoleKey()); consoleKey = Rx(() {
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
}
PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage?.read("console_key"); final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) { if(consoleKeyValue == null) {
return _kDefaultConsoleKey; return _kDefaultConsoleKey;
@@ -95,6 +93,13 @@ class BackendController extends GetxController {
} }
return consoleKey; return consoleKey;
}());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
_infoBars = [];
} }
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async { Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
@@ -103,6 +108,21 @@ class BackendController extends GetxController {
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true); await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
} }
String _readHost() {
String? value = _storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) {
return value;
}
if (type.value != ServerType.remote) {
return kDefaultBackendHost;
}
return "";
}
String _readPort() => _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
void joinLocalhost() { void joinLocalhost() {
gameServerAddress.text = kDefaultGameServerHost; gameServerAddress.text = kDefaultGameServerHost;
} }
@@ -121,179 +141,407 @@ class BackendController extends GetxController {
detached.value = false; detached.value = false;
} }
String _readHost() { Future<bool> toggle() {
String? value = _storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) {
return value;
}
if (type.value != ServerType.remote) {
return kDefaultBackendHost;
}
return "";
}
String _readPort() =>
_storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) async* {
try {
if(started.value) { if(started.value) {
return; return stop(interactive: true);
}
final serverType = type.value;
final hostData = this.host.text.trim();
final portData = this.port.text.trim();
started.value = true;
if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting);
}
if (hostData.isEmpty) {
yield ServerResult(ServerResultType.missingHostError);
started.value = false;
return;
}
if (portData.isEmpty) {
yield ServerResult(ServerResultType.missingPortError);
started.value = false;
return;
}
final portNumber = int.tryParse(portData);
if (portNumber == null) {
yield ServerResult(ServerResultType.illegalPortError);
started.value = false;
return;
}
if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
yield ServerResult(ServerResultType.freeingPort);
final result = await freeBackendPort();
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
if(!result) {
started.value = false;
return;
}
}
switch(serverType){
case ServerType.embedded:
final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) {
if(started.value) {
started.value = false;
onError(errorMessage);
}
});
watchProcess(process.pid).then((_) {
if(started.value) {
started.value = false;
onExit();
}
});
embeddedProcessPid = process.pid;
break;
case ServerType.remote:
yield ServerResult(ServerResultType.pingingRemote);
final uriResult = await pingBackend(hostData, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
started.value = false;
return;
}
remoteServer = await startRemoteBackendProxy(uriResult);
break;
case ServerType.local:
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"));
}else { }else {
// If the local server is running on port 3551 there is no reverse proxy running return start(interactive: true);
// We only need to check if everything is working }
started.value = false;
} }
break; Future<bool> start({required bool interactive}) async {
if(started.value) {
return true;
} }
yield ServerResult(ServerResultType.pingingLocal); _cancel();
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort); final stream = startBackend(
if(uriResult == null) { type: type.value,
yield ServerResult(ServerResultType.pingError); host: host.text,
remoteServer?.close(force: true); port: port.text,
localServer?.close(force: true); detached: detached.value,
started.value = false; onError: (errorMessage) {
return; stop(interactive: false);
} _showRebootInfoBar(
translations.backendErrorMessage,
yield ServerResult(ServerResultType.startSuccess); severity: InfoBarSeverity.error,
}catch(error, stackTrace) { duration: infoBarLongDuration,
yield ServerResult( action: Button(
ServerResultType.startError, onPressed: () => launchUrl(launcherLogFile.uri),
error: error, child: Text(translations.openLog),
stackTrace: stackTrace )
); );
remoteServer?.close(force: true);
localServer?.close(force: true);
started.value = false;
} }
);
final completer = Completer<bool>();
InfoBarEntry? entry;
_worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event, interactive);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
completer.complete(true);
}
});
return await completer.future;
} }
Stream<ServerResult> stop() async* { Future<bool> stop({required bool interactive}) async {
if(!started.value) { if(!started.value) {
return true;
}
_cancel();
final stream = stopBackend(
type: type.value,
implementation: _implementation
);
final completer = Completer<bool>();
InfoBarEntry? entry;
_worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event, interactive);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
completer.complete(true);
}
});
return await completer.future;
}
void _cancel() {
_worker?.cancel(); // Do not await or it will hang
_infoBars.forEach((infoBar) => infoBar.close());
_infoBars.clear();
}
InfoBarEntry? _handeEvent(ServerResult event, bool interactive) {
log("[BACKEND] Handling event: $event (interactive: $interactive, start: ${event.type.isStart}, error: ${event.type.isError})");
started.value = event.type.isStart && !event.type.isError;
switch (event.type) {
case ServerResultType.starting:
if(interactive) {
return _showRebootInfoBar(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startSuccess:
if(interactive) {
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.startError:
if(interactive) {
return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.stopping:
if(interactive) {
return _showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.stopSuccess:
if(interactive) {
return _showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.stopError:
if(interactive) {
return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.startMissingHostError:
if(interactive) {
return _showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startMissingPortError:
if(interactive) {
return _showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startIllegalPortError:
if(interactive) {
return _showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startFreeingPort:
if(interactive) {
return _showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startFreePortSuccess:
if(interactive) {
return _showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}else {
return null;
}
case ServerResultType.startFreePortError:
if(interactive) {
return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.startPingingRemote:
if(interactive) {
return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startPingingLocal:
if(interactive) {
return _showRebootInfoBar(
translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startPingError:
if(interactive) {
return _showRebootInfoBar(
translations.pingError(type.value.name),
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startedImplementation:
_implementation = event.implementation;
return null;
}
}
Future<void> joinServer(String uuid, FortniteServer server) async {
if(!kDebugMode && uuid == server.id) {
_showRebootInfoBar(
translations.joinSelfServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return; return;
} }
yield ServerResult(ServerResultType.stopping); final version = Get.find<GameController>()
started.value = false; .getVersionByName(server.version.toString());
try{ if(version == null) {
switch(type()){ _showRebootInfoBar(
case ServerType.embedded: translations.cannotJoinServerVersion(server.version.toString()),
final embeddedProcessPid = this.embeddedProcessPid; duration: infoBarLongDuration,
if(embeddedProcessPid != null) { severity: InfoBarSeverity.error
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm);
this.embeddedProcessPid = null;
}
break;
case ServerType.remote:
await remoteServer?.close(force: true);
remoteServer = null;
break;
case ServerType.local:
await localServer?.close(force: true);
localServer = null;
break;
}
yield ServerResult(ServerResultType.stopSuccess);
}catch(error, stackTrace){
yield ServerResult(
ServerResultType.stopError,
error: error,
stackTrace: stackTrace
); );
started.value = true; return;
}
} }
Stream<ServerResult> toggle({required void Function() onExit, required void Function(String) onError}) async* { final hashedPassword = server.password;
if(started()) { final hasPassword = hashedPassword != null;
yield* stop(); final embedded = type.value == ServerType.embedded;
}else { final author = server.author;
yield* start( final encryptedIp = server.ip;
onExit: onExit, if(!hasPassword) {
onError: onError final valid = await _isServerValid(encryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, encryptedIp, author, version);
return;
}
final confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
_showRebootInfoBar(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
final valid = await _isServerValid(decryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, decryptedIp, author, version);
}
Future<bool> _isServerValid(String address) async {
final result = await pingGameServer(address);
if(result) {
return true;
}
_showRebootInfoBar(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.serverPassword,
child: Obx(() => TextFormBox(
placeholder: translations.serverPasswordPlaceholder,
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
),
)
))
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: translations.serverPasswordCancel,
type: ButtonType.secondary
),
DialogButton(
text: translations.serverPasswordConfirm,
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
)
); );
} }
void _onServerJoined(bool embedded, String decryptedIp, String author, FortniteVersion version) {
if(embedded) {
gameServerAddress.text = decryptedIp;
pageIndex.value = RebootPageType.play.index;
}else {
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>()
.selectedVersion = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,
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;
}
Future<void> restart() async {
if(started.value) {
await stop(interactive: false);
await start(interactive: true);
}
} }
} }

View File

@@ -7,18 +7,19 @@ import 'package:get_storage/get_storage.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/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:path/path.dart' as path;
class DllController extends GetxController { class DllController extends GetxController {
static const String storageName = "v2_dll_storage"; static const String storageName = "v2_dll_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String originalDll; late final TextEditingController customGameServerDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll; late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll; late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort; late final TextEditingController gameServerPort;
late final Rx<UpdateTimer> timer; late final Rx<UpdateTimer> timer;
late final TextEditingController beforeS20Mirror; late final TextEditingController beforeS20Mirror;
@@ -26,14 +27,13 @@ class DllController extends GetxController {
late final RxBool customGameServer; late final RxBool customGameServer;
late final RxnInt timestamp; late final RxnInt timestamp;
late final Rx<UpdateStatus> status; late final Rx<UpdateStatus> status;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
DllController() { DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName); _storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot); customGameServerDll = _createController("game_server", InjectableDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console); unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.starfall); backendDll = _createController("backend", InjectableDll.auth);
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort); gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text)); gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer"); final timerIndex = _storage?.read("timer");
@@ -57,9 +57,9 @@ class DllController extends GetxController {
} }
void resetGame() { void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot); customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console); unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.starfall); backendDll.text = getDefaultDllPath(InjectableDll.auth);
} }
void resetServer() { void resetServer() {
@@ -74,16 +74,7 @@ class DllController extends GetxController {
} }
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async { Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
if(_updater != null) { InfoBarEntry? infoBarEntry;
return await _updater!;
}
final result = _updateGameServerDll(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateGameServerDll(bool force, bool silent) async {
try { try {
if(customGameServer.value) { if(customGameServer.value) {
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
@@ -109,8 +100,8 @@ class DllController extends GetxController {
} }
await Future.wait( await Future.wait(
[ [
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text), downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text, false),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text), downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text, true),
Future.delayed(const Duration(seconds: 1)) Future.delayed(const Duration(seconds: 1))
], ],
eagerError: false eagerError: false
@@ -148,42 +139,62 @@ class DllController extends GetxController {
) )
); );
return false; return false;
}finally {
_updater = null;
} }
} }
(File, bool) getInjectableData(Version version, InjectableDll dll) { (File, bool) getInjectableData(Version version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll)); final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){ switch(dll){
case InjectableDll.reboot: case InjectableDll.gameServer:
if(customGameServer.value) { if(customGameServer.value) {
return (File(gameServerDll.text), true); return (File(customGameServerDll.text), true);
} }
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false); return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
case InjectableDll.console: case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text); final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath); return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.starfall: case InjectableDll.auth:
final backendFile = File(backendDll.text); final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath); return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memoryLeak:
final memoryFile = File(memoryLeakDll.text);
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
} }
} }
String getDefaultDllPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll"; TextEditingController getDllEditingController(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
return unrealEngineConsoleDll;
case InjectableDll.auth:
return backendDll;
case InjectableDll.gameServer:
return customGameServerDll;
case InjectableDll.memoryLeak:
return memoryLeakDll;
}
}
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false, bool force = false}) async { String getDefaultDllPath(InjectableDll dll) {
log("[DLL] Asking for $filePath(silent: $silent)"); switch(dll) {
final fileName = basename(filePath).toLowerCase(); case InjectableDll.console:
log("[DLL] File name: $fileName"); return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth:
return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak:
return "${dllsDirectory.path}\\memory.dll";
}
}
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
InfoBarEntry? entry; InfoBarEntry? entry;
try { try {
if (fileName.contains("reboot")) { if (dll == InjectableDll.gameServer) {
log("[DLL] Downloading reboot.dll..."); return await updateGameServerDll(silent: silent);
return await updateGameServerDll(
silent: silent
);
} }
if(!force && File(filePath).existsSync()) { if(!force && File(filePath).existsSync()) {
@@ -199,7 +210,7 @@ class DllController extends GetxController {
duration: null duration: null
); );
} }
await downloadCriticalDll(fileName, filePath); await downloadDependency(dll, filePath);
entry?.close(); entry?.close();
if(!silent) { if(!silent) {
entry = await showRebootInfoBar( entry = await showRebootInfoBar(
@@ -218,13 +229,13 @@ class DllController extends GetxController {
error = error.toLowerCase(); error = error.toLowerCase();
final completer = Completer(); final completer = Completer();
await showRebootInfoBar( await showRebootInfoBar(
translations.downloadDllError(error.toString(), fileName), translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null), onDismissed: () => completer.complete(null),
action: Button( action: Button(
onPressed: () async { onPressed: () async {
await downloadCriticalDllInteractive(filePath); await download(dll, filePath, silent: silent, force: force);
completer.complete(null); completer.complete(null);
}, },
child: Text(translations.downloadDllRetry), child: Text(translations.downloadDllRetry),
@@ -234,6 +245,32 @@ class DllController extends GetxController {
return false; return false;
} }
} }
void guardFiles() {
for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
if (path.equals(controller.text, defaultPath)) {
download(injectable, controller.text);
}
controller.addListener(() async {
try {
if (!path.equals(controller.text, defaultPath)) {
return;
}
final filePath = controller.text;
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (path.equals(event.path, filePath)) {
await download(injectable, filePath);
}
}
} catch(_) {
// Ignore
}
});
}
}
} }
extension _UpdateTimerExtension on UpdateTimer { extension _UpdateTimerExtension on UpdateTimer {

View File

@@ -6,7 +6,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
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/info_bar.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:version/version.dart'; import 'package:version/version.dart';

View File

@@ -1,7 +1,7 @@
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog; import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';

View File

@@ -1,323 +0,0 @@
import 'dart:async';
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.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/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
final List<InfoBarEntry> _infoBars = [];
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 {
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>();
InfoBarEntry? entry;
worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
completer.complete(true);
}
});
return await completer.future;
}
InfoBarEntry _handeEvent(ServerResult event) {
log("[BACKEND] Handling event: $event");
switch (event.type) {
case ServerResultType.starting:
return _showRebootInfoBar(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.startSuccess:
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success
);
case ServerResultType.startError:
print(event.stackTrace);
return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.stopping:
return _showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.stopSuccess:
return _showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
case ServerResultType.stopError:
return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.missingHostError:
return _showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
case ServerResultType.missingPortError:
return _showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.illegalPortError:
return _showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.freeingPort:
return _showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
case ServerResultType.freePortSuccess:
return _showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
case ServerResultType.freePortError:
return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.pingingRemote:
return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingingLocal:
return _showRebootInfoBar(
translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingError:
return _showRebootInfoBar(
translations.pingError(type.value.name),
severity: InfoBarSeverity.error
);
}
}
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
if(!kDebugMode && uuid == server.id) {
_showRebootInfoBar(
translations.joinSelfServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final gameController = Get.find<GameController>();
final version = gameController.getVersionByName(server.version.toString());
if(version == null) {
_showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final hashedPassword = server.password;
final hasPassword = hashedPassword != null;
final embedded = type.value == ServerType.embedded;
final author = server.author;
final encryptedIp = server.ip;
if(!hasPassword) {
final valid = await _isServerValid(encryptedIp);
if(!valid) {
return;
}
_onSuccess(gameController, embedded, encryptedIp, author, version);
return;
}
final confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
_showRebootInfoBar(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
final valid = await _isServerValid(decryptedIp);
if(!valid) {
return;
}
_onSuccess(gameController, embedded, decryptedIp, author, version);
}
Future<bool> _isServerValid(String address) async {
final result = await pingGameServer(address);
if(result) {
return true;
}
_showRebootInfoBar(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.serverPassword,
child: Obx(() => TextFormBox(
placeholder: translations.serverPasswordPlaceholder,
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
),
)
))
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: translations.serverPasswordCancel,
type: ButtonType.secondary
),
DialogButton(
text: translations.serverPasswordConfirm,
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
)
);
}
void _onSuccess(GameController controller, bool embedded, String decryptedIp, String author, FortniteVersion version) {
if(embedded) {
gameServerAddress.text = decryptedIp;
pageIndex.value = RebootPageType.play.index;
}else {
FlutterClipboard.controlC(decryptedIp);
}
controller.selectedVersion = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,
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

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/widget/page/home_page.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
typedef WidgetBuilder = Widget Function(BuildContext, void Function()); typedef WidgetBuilder = Widget Function(BuildContext, void Function());
@@ -148,7 +148,7 @@ class _RenderAbsorbPointer extends RenderProxyBox {
// 32 is the height of the title bar (need this offset as the overlay area doesn't include it) // 32 is the height of the title bar (need this offset as the overlay area doesn't include it)
// Not an optimal solution but it works (calculating it is kind of complicated) // Not an optimal solution but it works (calculating it is kind of complicated)
position = Offset(position.dx, position.dy + HomePage.kTitleBarHeight); position = Offset(position.dx, position.dy);
final exclusionPosition = exclusion.localToGlobal(Offset.zero); final exclusionPosition = exclusion.localToGlobal(Offset.zero);
final exclusionSize = Rect.fromLTRB( final exclusionSize = Rect.fromLTRB(
exclusionPosition.dx, exclusionPosition.dx,

View File

@@ -1,8 +1,8 @@
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:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart'; import 'package:reboot_launcher/src/widget/message/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
abstract class RebootPage extends StatefulWidget { abstract class RebootPage extends StatefulWidget {

View File

@@ -3,16 +3,16 @@ import 'dart:collection';
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:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/implementation/backend_page.dart'; import 'package:reboot_launcher/src/widget/page/backend_page.dart';
import 'package:reboot_launcher/src/page/implementation/browser_page.dart'; import 'package:reboot_launcher/src/widget/page/browser_page.dart';
import 'package:reboot_launcher/src/page/implementation/host_page.dart'; import 'package:reboot_launcher/src/widget/page/host_page.dart';
import 'package:reboot_launcher/src/page/implementation/info_page.dart'; import 'package:reboot_launcher/src/widget/page/info_page.dart';
import 'package:reboot_launcher/src/page/implementation/play_page.dart'; import 'package:reboot_launcher/src/widget/page/play_page.dart';
import 'package:reboot_launcher/src/page/implementation/settings_page.dart'; import 'package:reboot_launcher/src/widget/page/settings_page.dart';
import 'package:reboot_launcher/src/widget/info_bar_area.dart'; import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
final StreamController<void> pagesController = StreamController.broadcast(); final StreamController<void> pagesController = StreamController.broadcast();
bool hitBack = false; bool hitBack = false;

View File

@@ -1,6 +1,7 @@
import 'dart:async'; 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';
@@ -9,8 +10,12 @@ const Duration _timeout = Duration(seconds: 5);
Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) { Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
final completer = Completer<bool>(); final completer = Completer<bool>();
final start = DateTime.now(); final start = DateTime.now();
(() async { _pingGameServerOrTimeout(completer, start, timeout, address);
while (!completer.isCompleted && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds) { 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); final result = await pingGameServer(address);
if(result) { if(result) {
completer.complete(true); completer.complete(true);
@@ -21,8 +26,6 @@ Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
if(!completer.isCompleted) { if(!completer.isCompleted) {
completer.complete(false); completer.complete(false);
} }
})();
return completer;
} }
Future<bool> pingGameServer(String address) async { Future<bool> pingGameServer(String address) async {

View File

@@ -492,3 +492,57 @@ int _convertToHString(String string) {
extension WindowManagerExtension on WindowManager { extension WindowManagerExtension on WindowManager {
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize(); 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

@@ -7,8 +7,8 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.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:reboot_launcher/src/widget/file_selector.dart'; import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
const double _kButtonDimensions = 30; const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8; const double _kButtonSpacing = 8;

View File

@@ -3,9 +3,9 @@ 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/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/profile.dart'; import 'package:reboot_launcher/src/widget/message/profile.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
class ProfileWidget extends StatefulWidget { class ProfileWidget extends StatefulWidget {

View File

@@ -1,6 +1,6 @@
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:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:skeletons/skeletons.dart'; import 'package:skeletons/skeletons.dart';
@@ -80,15 +80,19 @@ class SettingTileState extends State<SettingTile> {
) )
else else
widget.icon, widget.icon,
const SizedBox(width: 16.0), const SizedBox(width: 16.0),
Column(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
widget.title == null ? _skeletonTitle : widget.title!, widget.title == null ? _skeletonTitle : widget.title!,
widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!, widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!,
], ],
), ),
const Spacer(), ),
_trailing _trailing
], ],
), ),

View File

@@ -12,9 +12,8 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_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/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.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';
@@ -111,7 +110,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})..."); log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
final backendResult = _backendController.started() || await _backendController.toggleInteractive(); final backendResult = _backendController.started() || await _backendController.toggle();
if(!backendResult){ if(!backendResult){
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend"); log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop( _onStop(
@@ -242,7 +241,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{ }else{
_gameController.instance.value = instance; _gameController.instance.value = instance;
} }
await _injectOrShowError(InjectableDll.starfall, host); await _injectOrShowError(InjectableDll.auth, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance"); log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance; return instance;
} }
@@ -398,6 +397,7 @@ 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.memoryLeak, host);
if(!host){ if(!host){
await _injectOrShowError(InjectableDll.console, host); await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected(); _onGameClientInjected();
@@ -406,7 +406,7 @@ class _LaunchButtonState extends State<LaunchButton> {
if(gameServerPort != null) { if(gameServerPort != null) {
await killProcessByPort(gameServerPort); await killProcessByPort(gameServerPort);
} }
await _injectOrShowError(InjectableDll.reboot, host); await _injectOrShowError(InjectableDll.gameServer, host);
_onGameServerInjected(); _onGameServerInjected();
} }
} }
@@ -492,9 +492,10 @@ class _LaunchButtonState extends State<LaunchButton> {
final pingOperation = pingGameServerOrTimeout( final pingOperation = pingGameServerOrTimeout(
"$publicIp:$gameServerPort", "$publicIp:$gameServerPort",
const Duration(days: 365) const Duration(days: 1)
); );
this._pingOperation = pingOperation; this._pingOperation = pingOperation;
_gameServerInfoBar?.close();
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = showRebootInfoBar(
translations.checkGameServerFixMessage(gameServerPort), translations.checkGameServerFixMessage(gameServerPort),
action: Button( action: Button(
@@ -508,8 +509,9 @@ class _LaunchButtonState extends State<LaunchButton> {
final result = await pingOperation.future; final result = await pingOperation.future;
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return result; return result;
}finally { }catch(_) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return false;
} }
} }
@@ -524,7 +526,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
await _operation?.cancel(); await _operation?.cancel();
_operation = null; _operation = null;
_backendController.cancelInteractive(); _backendController.stop(interactive: false);
} }
host = host ?? widget.host; host = host ?? widget.host;
@@ -627,7 +629,7 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.tokenError: case _StopReason.tokenError:
_backendController.stop(); _backendController.stop(interactive: false);
showRebootInfoBar( showRebootInfoBar(
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")), translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
@@ -712,7 +714,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
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 _dllController.downloadCriticalDllInteractive(file.path, force: true); await _dllController.download(injectable, file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(version, injectable, host, true); return _getDllFileOrStop(version, injectable, host, true);
} }

View File

@@ -1,5 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
Future<void> showResetDialog(Function() onConfirm) => showRebootDialog( Future<void> showResetDialog(Function() onConfirm) => showRebootDialog(

View File

@@ -1,5 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog( Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';

View File

@@ -5,16 +5,16 @@ import 'package:reboot_launcher/src/controller/backend_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/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/profile.dart'; import 'package:reboot_launcher/src/widget/message/profile.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/implementation/backend_page.dart'; import 'package:reboot_launcher/src/widget/page/backend_page.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/widget/page/home_page.dart';
import 'package:reboot_launcher/src/page/implementation/host_page.dart'; import 'package:reboot_launcher/src/widget/page/host_page.dart';
import 'package:reboot_launcher/src/page/implementation/play_page.dart'; import 'package:reboot_launcher/src/widget/page/play_page.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart'; import 'package:reboot_launcher/src/widget/version/version_selector.dart';
void startOnboarding() { void startOnboarding() {
final gameController = Get.find<GameController>(); final gameController = Get.find<GameController>();

View File

@@ -2,8 +2,7 @@ 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';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/messenger/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';
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{ Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{

View File

@@ -7,11 +7,11 @@ import 'package:flutter/foundation.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/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/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/util/types.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart'; import 'package:reboot_launcher/src/widget/file/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 {
@@ -35,20 +35,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
final Rxn<double> _progress = Rxn(); final Rxn<double> _progress = Rxn();
final RxInt _speed = RxInt(0); final RxInt _speed = RxInt(0);
late DiskSpace _diskSpace;
late Future<List<FortniteBuild>> _fetchFuture;
late Future _diskFuture;
SendPort? _downloadPort; SendPort? _downloadPort;
Object? _error; Object? _error;
StackTrace? _stackTrace; StackTrace? _stackTrace;
@override @override
void initState() { void initState() {
_fetchFuture = compute(fetchBuilds, null);
_diskSpace = DiskSpace();
_diskFuture = _diskSpace.scan()
.then((_) => _updateFormDefaults());
super.initState(); super.initState();
} }
@@ -62,6 +54,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
void _cancelDownload() { void _cancelDownload() {
_downloadPort?.send(kStopBuildDownloadSignal); _downloadPort?.send(kStopBuildDownloadSignal);
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
stopDownloadServer();
} }
@override @override
@@ -70,28 +63,10 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
child: Obx(() { child: Obx(() {
switch(_status.value){ switch(_status.value){
case _DownloadStatus.form: case _DownloadStatus.form:
return FutureBuilder(
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture),
builder: (context, snapshot) {
if (snapshot.hasError) {
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
}
final data = snapshot.data;
if (data == null) {
return ProgressDialog(
text: translations.fetchingBuilds,
showButton: widget.closable,
onStop: () => Navigator.of(context).pop()
);
}
return Obx(() => FormDialog( return Obx(() => FormDialog(
content: _buildFormBody(data), content: _buildFormBody(downloadableBuilds),
buttons: _formButtons buttons: _formButtons
)); ));
}
);
case _DownloadStatus.downloading: case _DownloadStatus.downloading:
case _DownloadStatus.extracting: case _DownloadStatus.extracting:
return GenericDialog( return GenericDialog(
@@ -244,12 +219,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
), ),
), ),
if(_progress.value != null && !_isAllocatingDiskSpace) if(_progress.value != null)
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
if(_progress.value != null && !_isAllocatingDiskSpace) if(_progress.value != null)
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -258,7 +233,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
style: FluentTheme.maybeOf(context)?.typography.body, style: FluentTheme.maybeOf(context)?.typography.body,
), ),
if(timeLeft != null) if(timeLeft != null && timeLeft != -1)
Text( Text(
translations.timeLeft(timeLeft), translations.timeLeft(timeLeft),
style: FluentTheme.maybeOf(context)?.typography.body, style: FluentTheme.maybeOf(context)?.typography.body,
@@ -272,7 +247,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ProgressBar(value: _isAllocatingDiskSpace ? null : _progress.value?.toDouble()) child: ProgressBar(value: _progress.value?.toDouble())
), ),
const SizedBox( const SizedBox(
@@ -291,15 +266,9 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return translations.startingDownload; return translations.startingDownload;
} }
if (_speed.value == 0) {
return translations.allocatingSpace;
}
return translations.downloading; return translations.downloading;
} }
bool get _isAllocatingDiskSpace => _status.value == _DownloadStatus.downloading && _speed.value == 0;
Widget _buildFormBody(List<FortniteBuild> builds) { Widget _buildFormBody(List<FortniteBuild> builds) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -450,16 +419,15 @@ 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}"; 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

@@ -5,16 +5,16 @@ 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/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/keyboard.dart'; import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/server_start_button.dart'; import 'package:reboot_launcher/src/widget/server/server_start_button.dart';
import 'package:reboot_launcher/src/widget/server_type_selector.dart'; import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey(); final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
@@ -162,7 +162,12 @@ class _BackendPageState extends RebootPageState<BackendPage> {
key: backendDetachedOverlayTargetKey, key: backendDetachedOverlayTargetKey,
child: ToggleSwitch( child: ToggleSwitch(
checked: _backendController.detached(), checked: _backendController.detached(),
onChanged: (value) => _backendController.detached.value = value onChanged: (value) async {
_backendController.detached.value = value;
if(_backendController.started.value) {
await _backendController.restart();
}
}
), ),
), ),
], ],

View File

@@ -1,4 +1,3 @@
import 'dart:async'; import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
@@ -9,12 +8,11 @@ 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/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/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/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/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
class BrowsePage extends RebootPage { class BrowsePage extends RebootPage {
const BrowsePage({Key? key}) : super(key: key); const BrowsePage({Key? key}) : super(key: key);
@@ -211,10 +209,18 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
icon: Icon( icon: Icon(
hasPassword ? FluentIcons.lock : FluentIcons.globe hasPassword ? FluentIcons.lock : FluentIcons.globe
), ),
title: Text("${_formatName(entry)}${entry.author}"), title: Text(
subtitle: Text("${_formatDescription(entry)}${_formatVersion(entry)}"), "${_formatName(entry)}${entry.author}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
subtitle: Text(
"${_formatDescription(entry)}${_formatVersion(entry)}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
content: Button( content: Button(
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry), onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp), child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
) )
); );

View File

@@ -10,22 +10,21 @@ 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/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/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/dll.dart'; import 'package:reboot_launcher/src/widget/message/dll.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/page_suggestion.dart';
import 'package:reboot_launcher/src/page/abstract/page_suggestion.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:reboot_launcher/src/widget/info_bar_area.dart'; import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/profile_tile.dart';
import 'package:reboot_launcher/src/widget/title_bar.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';
@@ -33,7 +32,6 @@ final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
static const double kDefaultPadding = 12.0; static const double kDefaultPadding = 12.0;
static const double kTitleBarHeight = 32;
const HomePage({Key? key}) : super(key: key); const HomePage({Key? key}) : super(key: key);
@@ -43,6 +41,7 @@ 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 DllController _dllController = Get.find<DllController>();
@@ -76,6 +75,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
lastPage = index; lastPage = index;
_pageController.jumpToPage(index); _pageController.jumpToPage(index);
pagesController.add(null);
}); });
} }
@@ -93,7 +93,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
final uuid = uri.host; final uuid = uri.host;
final server = _hostingController.findServerById(uuid); final server = _hostingController.findServerById(uuid);
if(server != null) { if(server != null) {
_backendController.joinServerInteractive(_hostingController.uuid, server); _backendController.joinServer(_hostingController.uuid, server);
}else { }else {
showRebootInfoBar( showRebootInfoBar(
translations.noServerFound, translations.noServerFound,
@@ -134,37 +134,50 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
dllsDirectory.createSync(recursive: true); dllsDirectory.createSync(recursive: true);
} }
final dummy = Version.parse("1"); _dllController.guardFiles();
final dummyS20 = Version.parse("20");
for(final injectable in InjectableDll.values) {
_downloadDll(dummy, injectable);
if(injectable.isVersionDependent) {
_downloadDll(dummyS20, injectable);
}
}
watchDlls().listen((filePath) => showDllDeletedDialog(() {
_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 {
await windowManager.hide();
}catch(error) {
log("[WINDOW] Cannot hide window: $error");
}
try { try {
await _hostingController.discardServer(); await _hostingController.discardServer();
}catch(error) { }catch(error) {
log("[HOSTING] Cannot discard server: $error"); log("[HOSTING] Cannot discard server on exit: $error");
} }
try {
if(_backendController.started.value) {
await _backendController.toggle();
}
}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
@@ -252,35 +265,13 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
_focused.value = true; _focused.value = true;
} }
@override
void onWindowEvent(String eventName) {
if(eventName != "move") {
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
_settingsController.language.value; _settingsController.language.value;
loadTranslations(context); loadTranslations(context);
return Obx(() {
return Container( return Container(
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93), color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: HomePage.kTitleBarHeight,
child: Row(
children: [
_backButton,
Expanded(child: _draggableArea),
WindowTitleBar(focused: _focused())
],
)
),
Expanded(
child: Navigator( child: Navigator(
key: appNavigatorKey, key: appNavigatorKey,
onPopPage: (page, data) => false, onPopPage: (page, data) => false,
@@ -303,11 +294,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
) )
], ],
) )
)
],
),
); );
});
} }
Widget _buildBody() => Expanded( Widget _buildBody() => Expanded(
@@ -538,42 +525,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
); );
} }
Widget get _backButton => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0
)),
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.all(Border())
),
onPressed: appStack.isEmpty && !inDialog ? null : () {
if(inDialog) {
Navigator.of(appNavigatorKey.currentContext!).pop();
}else {
final lastPage = appStack.removeLast();
pageStack.remove(lastPage);
if (lastPage is int) {
hitBack = true;
pageIndex.value = lastPage;
} else {
Navigator.of(pageKey.currentContext!).pop();
}
}
pagesController.add(null);
},
child: const Icon(FluentIcons.back, size: 12.0),
)
);
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: windowManager.maximizeOrRestore,
onHorizontalDragStart: (_) => windowManager.startDragging(),
onVerticalDragStart: (_) => windowManager.startDragging()
);
Widget get _autoSuggestBox => Padding( Widget get _autoSuggestBox => Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,

View File

@@ -10,18 +10,16 @@ import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_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/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/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/game_start_button.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey(); final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey(); final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();

View File

@@ -1,11 +1,11 @@
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/material.dart'; import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart'; import 'package:reboot_launcher/src/widget/message/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/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/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class InfoPage extends RebootPage { class InfoPage extends RebootPage {
@@ -29,7 +29,7 @@ class InfoPage extends RebootPage {
class _InfoPageState extends RebootPageState<InfoPage> { class _InfoPageState extends RebootPageState<InfoPage> {
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new"; static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
static const String _kDiscordInviteUrl = "https://discord.gg/reboot"; static const String _kDiscordInviteUrl = "https://discord.gg/rebootmp";
@override @override
List<SettingTile> get settings => [ List<SettingTile> get settings => [

View File

@@ -1,15 +1,16 @@
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:get/get.dart'; import 'package:get/get.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/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/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/game_start_button.dart'; import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart'; import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey(); final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
@@ -34,6 +35,7 @@ class PlayPage extends RebootPage {
class _PlayPageState extends RebootPageState<PlayPage> { class _PlayPageState extends RebootPageState<PlayPage> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final DllController _dllController = Get.find<DllController>();
@override @override
Widget? get button => LaunchButton( Widget? get button => LaunchButton(
@@ -81,6 +83,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
content: Button( content: Button(
onPressed: () => showResetDialog(() { onPressed: () => showResetDialog(() {
_gameController.reset(); _gameController.reset();
_dllController.resetGame();
}), }),
child: Text(translations.gameResetDefaultsContent), child: Text(translations.gameResetDefaultsContent),
) )

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:async/async.dart';
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_gen/gen_l10n/reboot_localizations.dart'; import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
@@ -6,12 +9,12 @@ 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/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/dialog.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/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/file/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class SettingsPage extends RebootPage { class SettingsPage extends RebootPage {
@@ -36,6 +39,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>(); final DllController _dllController = Get.find<DllController>();
int? _downloadFromMirrorId;
@override @override
Widget? get button => null; Widget? get button => null;
@@ -62,7 +66,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
onReset: () { onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.console); final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path; _dllController.unrealEngineConsoleDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true); _dllController.download(InjectableDll.console, path, force: true);
} }
), ),
createFileSetting( createFileSetting(
@@ -70,9 +74,19 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
description: translations.settingsClientAuthDescription, description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll, controller: _dllController.backendDll,
onReset: () { onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.starfall); final path = _dllController.getDefaultDllPath(InjectableDll.auth);
_dllController.backendDll.text = path; _dllController.backendDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true); _dllController.download(InjectableDll.auth, path, force: true);
}
),
createFileSetting(
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
_dllController.memoryLeakDll.text = path;
_dllController.download(InjectableDll.memoryLeak, path, force: true);
} }
), ),
_internalFilesServerType, _internalFilesServerType,
@@ -105,7 +119,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
} }
_dllController.customGameServer.value = entry.key; _dllController.customGameServer.value = entry.key;
_dllController.infoBarEntry?.close();
if(!entry.key) { if(!entry.key) {
_dllController.updateGameServerDll( _dllController.updateGameServerDll(
force: true force: true
@@ -131,11 +144,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
child: TextFormBox( child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder, placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror, controller: _dllController.beforeS20Mirror,
onChanged: (value) { onChanged: _scheduleMirrorDownload
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
), ),
), ),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
@@ -174,16 +183,34 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
return createFileSetting( return createFileSetting(
title: translations.settingsOldServerFileName, title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription, description: translations.settingsServerFileDescription,
controller: _dllController.gameServerDll, controller: _dllController.customGameServerDll,
onReset: () { onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.reboot); final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
_dllController.gameServerDll.text = path; _dllController.customGameServerDll.text = path;
_dllController.downloadCriticalDllInteractive(path); _dllController.download(InjectableDll.gameServer, path);
} }
); );
} }
}); });
void _scheduleMirrorDownload(String value) async {
if(_downloadFromMirrorId != null) {
return;
}
if(Uri.tryParse(value) == null) {
return;
}
final id = Random.secure().nextInt(1000000);
_downloadFromMirrorId = id;
await Future.delayed(const Duration(seconds: 2));
if(_downloadFromMirrorId == id) {
await _dllController.updateGameServerDll(force: true);
}
_downloadFromMirrorId = null;
}
Widget get _internalFilesNewServerSource => Obx(() { Widget get _internalFilesNewServerSource => Obx(() {
if(!_dllController.customGameServer.value) { if(!_dllController.customGameServer.value) {
return SettingTile( return SettingTile(
@@ -199,11 +226,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
child: TextFormBox( child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder, placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror, controller: _dllController.aboveS20Mirror,
onChanged: (value) { onChanged: _scheduleMirrorDownload
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
), ),
), ),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
@@ -263,7 +286,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
text: Text(entry.text), text: Text(entry.text),
onPressed: () { onPressed: () {
_dllController.timer.value = entry; _dllController.timer.value = entry;
_dllController.infoBarEntry?.close();
_dllController.updateGameServerDll( _dllController.updateGameServerDll(
force: true force: true
); );

View File

@@ -4,7 +4,6 @@ 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/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
class ServerButton extends StatefulWidget { class ServerButton extends StatefulWidget {
@@ -46,7 +45,7 @@ class _ServerButtonState extends State<ServerButton> {
builder: (context, snapshot) => Obx(() => Text(_buttonText)) builder: (context, snapshot) => Obx(() => Text(_buttonText))
), ),
), ),
onPressed: () => _controller.toggleInteractive() onPressed: () => _controller.toggle()
) )
) )
); );

View File

@@ -2,8 +2,8 @@ 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/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
class ServerTypeSelector extends StatefulWidget { class ServerTypeSelector extends StatefulWidget {
@@ -32,18 +32,16 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
)); ));
} }
MenuFlyoutItem _createItem(ServerType type) { MenuFlyoutItem _createItem(ServerType type) => MenuFlyoutItem(
return MenuFlyoutItem(
text: Text(type.label), text: Text(type.label),
onPressed: () async { onPressed: () async {
_controller.stop(); await _controller.stop(interactive: false);
_controller.type.value = type; _controller.type.value = type;
} }
); );
} }
}
extension ServerTypeExtension on ServerType { extension _ServerTypeExtension on ServerType {
String get label { String get label {
return this == ServerType.embedded ? translations.embedded return this == ServerType.embedded ? translations.embedded
: this == ServerType.remote ? translations.remote : this == ServerType.remote ? translations.remote

View File

@@ -1,51 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/title_bar_buttons.dart';
import 'package:system_theme/system_theme.dart';
class WindowTitleBar extends StatelessWidget {
final bool focused;
const WindowTitleBar({Key? key, required this.focused}) : super(key: key);
@override
Widget build(BuildContext context) {
var lightMode = FluentTheme.of(context).brightness.isLight;
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MinimizeWindowButton(
colors: WindowButtonColors(
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
iconMouseDown: lightMode ? Colors.black : Colors.white,
iconMouseOver: lightMode ? Colors.black : Colors.white,
normal: Colors.transparent,
mouseOver: _color,
mouseDown: _color.withOpacity(0.7)),
),
MaximizeWindowButton(
colors: WindowButtonColors(
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
iconMouseDown: lightMode ? Colors.black : Colors.white,
iconMouseOver: lightMode ? Colors.black : Colors.white,
normal: Colors.transparent,
mouseOver: _color,
mouseDown: _color.withOpacity(0.7)),
),
CloseWindowButton(
colors: WindowButtonColors(
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
iconMouseDown: lightMode ? Colors.black : Colors.white,
iconMouseOver: lightMode ? Colors.black : Colors.white,
normal: Colors.transparent,
mouseOver: Colors.red,
mouseDown: Colors.red.withOpacity(0.7),
),
),
],
);
}
Color get _color =>
SystemTheme.accentColor.accent;
}

View File

@@ -1,174 +0,0 @@
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_mouse.dart';
typedef WindowButtonIconBuilder = Widget Function(
WindowButtonContext buttonContext);
typedef WindowButtonBuilder = Widget Function(
WindowButtonContext buttonContext, Widget icon);
class WindowButtonContext {
BuildContext context;
MouseState mouseState;
Color? backgroundColor;
Color iconColor;
WindowButtonContext(
{required this.context,
required this.mouseState,
this.backgroundColor,
required this.iconColor});
}
class WindowButtonColors {
late Color normal;
late Color mouseOver;
late Color mouseDown;
late Color iconNormal;
late Color iconMouseOver;
late Color iconMouseDown;
WindowButtonColors(
{Color? normal,
Color? mouseOver,
Color? mouseDown,
Color? iconNormal,
Color? iconMouseOver,
Color? iconMouseDown}) {
this.normal = normal ?? _defaultButtonColors.normal;
this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
}
}
final _defaultButtonColors = WindowButtonColors(
normal: Colors.transparent,
iconNormal: const Color(0xFF805306),
mouseOver: const Color(0xFF404040),
mouseDown: const Color(0xFF202020),
iconMouseOver: const Color(0xFFFFFFFF),
iconMouseDown: const Color(0xFFF0F0F0));
class WindowButton extends StatelessWidget {
final WindowButtonBuilder? builder;
final WindowButtonIconBuilder? iconBuilder;
late final WindowButtonColors colors;
final bool animate;
final EdgeInsets? padding;
final VoidCallback? onPressed;
WindowButton(
{Key? key,
WindowButtonColors? colors,
this.builder,
@required this.iconBuilder,
this.padding,
this.onPressed,
this.animate = false})
: super(key: key) {
this.colors = colors ?? _defaultButtonColors;
}
Color getBackgroundColor(MouseState mouseState) {
if (mouseState.isMouseDown) return colors.mouseDown;
if (mouseState.isMouseOver) return colors.mouseOver;
return colors.normal;
}
Color getIconColor(MouseState mouseState) {
if (mouseState.isMouseDown) return colors.iconMouseDown;
if (mouseState.isMouseOver) return colors.iconMouseOver;
return colors.iconNormal;
}
@override
Widget build(BuildContext context) {
return MouseStateBuilder(
builder: (context, mouseState) {
WindowButtonContext buttonContext = WindowButtonContext(
mouseState: mouseState,
context: context,
backgroundColor: getBackgroundColor(mouseState),
iconColor: getIconColor(mouseState));
var icon =
(iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
var fadeOutColor =
getBackgroundColor(MouseState()..isMouseOver = true)
.withOpacity(0);
var padding = this.padding ?? EdgeInsets.zero;
var animationMs = mouseState.isMouseOver
? (animate ? 100 : 0)
: (animate ? 200 : 0);
Widget iconWithPadding = Padding(padding: padding, child: icon);
iconWithPadding = AnimatedContainer(
curve: Curves.easeOut,
duration: Duration(milliseconds: animationMs),
color: buttonContext.backgroundColor ?? fadeOutColor,
child: iconWithPadding);
var button = (builder != null)
? builder!(buttonContext, icon)
: iconWithPadding;
return SizedBox.square(dimension: 45, child: button);
},
onPressed: onPressed);
}
}
class MinimizeWindowButton extends WindowButton {
MinimizeWindowButton(
{Key? key,
WindowButtonColors? colors,
VoidCallback? onPressed,
bool? animate})
: super(
key: key,
colors: colors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MinimizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => windowManager.minimize());
}
class MaximizeWindowButton extends WindowButton {
MaximizeWindowButton(
{Key? key,
WindowButtonColors? colors,
VoidCallback? onPressed,
bool? animate})
: super(
key: key,
colors: colors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MaximizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ??
() => windowManager.maximizeOrRestore());
}
final _defaultCloseButtonColors = WindowButtonColors(
mouseOver: const Color(0xFFD32F2F),
mouseDown: const Color(0xFFB71C1C),
iconNormal: const Color(0xFF805306),
iconMouseOver: const Color(0xFFFFFFFF));
class CloseWindowButton extends WindowButton {
CloseWindowButton(
{Key? key,
WindowButtonColors? colors,
VoidCallback? onPressed,
bool? animate})
: super(
key: key,
colors: colors ?? _defaultCloseButtonColors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
CloseIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => windowManager.close());
}

View File

@@ -1,118 +0,0 @@
import 'dart:math';
import 'package:flutter/widgets.dart';
class CloseIcon extends StatelessWidget {
final Color color;
const CloseIcon({Key? key, required this.color}) : super(key: key);
@override
Widget build(BuildContext context) => Align(
alignment: Alignment.topLeft,
child: Stack(children: [
Transform.rotate(
angle: pi * .25,
child:
Center(child: Container(width: 14, height: 1, color: color))),
Transform.rotate(
angle: pi * -.25,
child:
Center(child: Container(width: 14, height: 1, color: color))),
]),
);
}
class MaximizeIcon extends StatelessWidget {
final Color color;
const MaximizeIcon({Key? key, required this.color}) : super(key: key);
@override
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
}
class _MaximizePainter extends _IconPainter {
_MaximizePainter(Color color) : super(color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
}
}
class RestoreIcon extends StatelessWidget {
final Color color;
const RestoreIcon({
Key? key,
required this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
}
class _RestorePainter extends _IconPainter {
_RestorePainter(Color color) : super(color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
canvas.drawLine(
Offset(size.width, 0), Offset(size.width, size.height - 2), p);
canvas.drawLine(Offset(size.width, size.height - 2),
Offset(size.width - 2, size.height - 2), p);
}
}
class MinimizeIcon extends StatelessWidget {
final Color color;
const MinimizeIcon({Key? key, required this.color}) : super(key: key);
@override
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
}
class _MinimizePainter extends _IconPainter {
_MinimizePainter(Color color) : super(color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
canvas.drawLine(
Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
}
}
abstract class _IconPainter extends CustomPainter {
_IconPainter(this.color);
final Color color;
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class _AlignedPaint extends StatelessWidget {
const _AlignedPaint(this.painter, {Key? key}) : super(key: key);
final CustomPainter painter;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: CustomPaint(size: const Size(10, 10), painter: painter));
}
}
Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
..color = color
..style = PaintingStyle.stroke
..isAntiAlias = isAntiAlias
..strokeWidth = 1;

View File

@@ -1,71 +0,0 @@
import 'package:flutter/widgets.dart';
typedef MouseStateBuilderCB = Widget Function(
BuildContext context, MouseState mouseState);
class MouseState {
bool isMouseOver;
bool isMouseDown;
MouseState() : isMouseOver = false, isMouseDown = false;
}
class MouseStateBuilder extends StatefulWidget {
final MouseStateBuilderCB builder;
final VoidCallback? onPressed;
const MouseStateBuilder({Key? key, required this.builder, this.onPressed})
: super(key: key);
@override
State<MouseStateBuilder> createState() => _MouseStateBuilderState();
}
class _MouseStateBuilderState extends State<MouseStateBuilder> {
late MouseState _mouseState;
_MouseStateBuilderState() {
_mouseState = MouseState();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setState(() {
_mouseState.isMouseOver = true;
});
},
onExit: (event) {
setState(() {
_mouseState.isMouseOver = false;
});
},
child: GestureDetector(
onTapDown: (_) {
setState(() {
_mouseState.isMouseDown = true;
});
},
onTapCancel: () {
setState(() {
_mouseState.isMouseDown = false;
});
},
onTap: () {
setState(() {
_mouseState.isMouseDown = false;
_mouseState.isMouseOver = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.onPressed != null) {
widget.onPressed!();
}
});
},
onTapUp: (_) {},
child: widget.builder(context, _mouseState)
)
);
}
}

View File

@@ -6,9 +6,9 @@ import 'package:flutter/gestures.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/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/version.dart'; import 'package:reboot_launcher/src/widget/message/version.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';

View File

@@ -1,9 +1,9 @@
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:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart'; import 'package:reboot_launcher/src/widget/version/version_selector.dart';
SettingTile buildVersionSelector({ SettingTile buildVersionSelector({
required GlobalKey<OverlayTargetState> key required GlobalKey<OverlayTargetState> key

View File

@@ -1,11 +1,15 @@
name: reboot_launcher name: reboot_launcher
description: Graphical User Interface for Project Reboot description: Graphical User Interface for Project Reboot
version: "10.0.0" version: "10.0.7"
publish_to: 'none' publish_to: 'none'
environment: environment:
sdk: ">=3.0.0 <=4.0.0" # 3.19.0 is the last version that supports Windows 7/8/8.1 officially
# I have no clue who is still using Windows 7, but some users requested support, so might as well add it
# Repository Issue: https://github.com/Auties00/Reboot-Launcher/issues/58
# Flutter issue: https://github.com/flutter/flutter/issues/140830#issuecomment-1936397549
sdk: ">=3.0.0 <=3.19.0"
dependencies: dependencies:
# The flutter SDK # The flutter SDK
@@ -18,8 +22,7 @@ dependencies:
# Windows UI 3 # Windows UI 3
fluent_ui: ^4.9.1 fluent_ui: ^4.9.1
flutter_acrylic: flutter_acrylic: ^1.1.4
path: ./dependencies/flutter_acrylic
fluentui_system_icons: ^1.1.258 fluentui_system_icons: ^1.1.258
system_theme: ^3.1.1 system_theme: ^3.1.1
skeletons: skeletons:
@@ -40,6 +43,7 @@ dependencies:
# Async helpers # Async helpers
async: ^2.11.0 async: ^2.11.0
sync: ^0.3.0 sync: ^0.3.0
synchronized: ^3.3.0+3
# State management # State management
get: ^4.6.6 get: ^4.6.6
@@ -58,7 +62,6 @@ dependencies:
# Storage # Storage
get_storage: ^2.1.1 get_storage: ^2.1.1
universal_disk_space: ^0.2.3
path: ^1.9.0 path: ^1.9.0
# Translations # Translations

View File

@@ -9,7 +9,7 @@
#include <app_links/app_links_plugin_c_api.h> #include <app_links/app_links_plugin_c_api.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_windows/screen_retriever_windows_plugin_c_api.h>
#include <system_theme/system_theme_plugin.h> #include <system_theme/system_theme_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
@@ -22,8 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
LocalNotifierPluginRegisterWithRegistrar( LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin")); registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar( ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SystemThemePluginRegisterWithRegistrar( SystemThemePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SystemThemePlugin")); registry->GetRegistrarForPlugin("SystemThemePlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View File

@@ -6,7 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
app_links app_links
flutter_acrylic flutter_acrylic
local_notifier local_notifier
screen_retriever screen_retriever_windows
system_theme system_theme
url_launcher_windows url_launcher_windows
window_manager window_manager

View File

@@ -36,7 +36,6 @@ Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
Source: "..\..\dependencies\redist\VC_redist.x64.exe"; DestDir: {tmp}; Flags: dontcopy 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: "{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 Filename: "{tmp}\VC_redist.x64.exe"; StatusMsg: "{cm:InstallingVC2017redist}"; Parameters: "/quiet"; Check: VC2017RedistNeedsInstall; Flags: waituntilterminated
@@ -46,6 +45,44 @@ 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]
var
Page: TInputOptionWizardPage;
procedure InitializeWizard();
begin
Page := CreateInputOptionPage(
wpWelcome,
' Allow DLL injection',
' The Reboot Launcher needs to inject DLLs into Fortnite to create the game server',
'Selecting the option below will add the Reboot Launcher to the Windows Exclusions list. ' +
'This is necessary because DLL injection is often detected as a virus, but is necessary to modify Fortnite. ' +
'This option was designed for advanced users who want to manually manage the exclusions list on their machine. ' +
'If you do not trust the Reboot Launcher, you can audit the source code at https://github.com/Auties00/reboot_launcher and build it from source.',
False,
False
);
Page.Add('&Add the launcher to the Windows Exclusions list');
Page.Values[0] := True;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
InstallationDir: String;
begin
if (CurStep = ssPostInstall) and Page.Values[0] then
begin
InstallationDir := ExpandConstant('{app}');
Exec('powershell.exe', '-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath ''' + InstallationDir + '''""' , '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Log('Powershell exit code: ' + IntToStr(ResultCode));
end;
end;
function CompareVersion(version1, version2: String): Integer; function CompareVersion(version1, version2: String): Integer;
var var
packVersion1, packVersion2: Int64; packVersion1, packVersion2: Int64;

View File

@@ -6,6 +6,10 @@
#include <dwmapi.h> #include <dwmapi.h>
#include <iostream>
#include "Windowsx.h"
namespace { namespace {
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
@@ -160,50 +164,43 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
} }
LRESULT LRESULT
Win32Window::MessageHandler(HWND hwnd, Win32Window::MessageHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept {
UINT const message, switch (uMsg) {
WPARAM const wparam, case WM_DESTROY: {
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr; window_handle_ = nullptr;
Destroy(); Destroy();
if (quit_on_close_) { if (quit_on_close_) {
PostQuitMessage(0); PostQuitMessage(0);
} }
return 0; return 0;
}
case WM_DPICHANGED: { case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT *>(lparam); auto newRectSize = reinterpret_cast<RECT *>(lParam);
LONG newWidth = newRectSize->right - newRectSize->left; LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top; LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hWnd, nullptr, newRectSize->left, newRectSize->top, newWidth,newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0; return 0;
} }
case WM_SIZE: { case WM_SIZE: {
RECT rect = GetClientArea(); auto rect = GetClientArea();
if (child_content_ != nullptr) { if (child_content_ != nullptr) {
// Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,rect.bottom - rect.top, TRUE);
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
} }
return 0; return DefWindowProc(child_content_, uMsg, wParam, lParam);
} }
case WM_ACTIVATE: case WM_ACTIVATE: {
if (child_content_ != nullptr) { if (child_content_ != nullptr) {
SetFocus(child_content_); SetFocus(child_content_);
} }
return 0; return 0;
case WM_NCCALCSIZE:
return 0;
} }
return DefWindowProc(window_handle_, message, wparam, lparam); default:
return DefWindowProc(window_handle_, uMsg, wParam, lParam);
}
} }
void Win32Window::Destroy() { void Win32Window::Destroy() {
@@ -228,8 +225,7 @@ void Win32Window::SetChildContent(HWND content) {
SetParent(content, window_handle_); SetParent(content, window_handle_);
RECT frame = GetClientArea(); RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left, MoveWindow(content, frame.left, frame.top, frame.right - frame.left,frame.bottom - frame.top, true);
frame.bottom - frame.top, true);
SetFocus(child_content_); SetFocus(child_content_);
} }