5 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
31 changed files with 1506 additions and 905 deletions

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:
```
python move.py
```
and provide the required parameters.

View File

@@ -1,66 +0,0 @@
import argparse
import os
import requests
import boto3
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urlparse
def upload_url_to_s3(s3_client, bucket_name, url, object_key):
response = requests.get(url, stream=True, verify=False, headers={"Cookie": "_c_t_c=1"})
response.raise_for_status()
s3_client.upload_fileobj(response.raw, bucket_name, object_key)
return url, object_key
def derive_key_from_url(url, prefix=None):
parsed = urlparse(url)
filename = os.path.basename(parsed.path)
if prefix:
return f"{prefix}/{filename}"
else:
return filename
def main():
parser = argparse.ArgumentParser(description="Upload multiple URLs from versions.txt to an S3 bucket concurrently.")
parser.add_argument('--bucket', required=True, help="Name of the S3 bucket.")
parser.add_argument('--concurrency', required=True, type=int, help="Number of concurrent uploads.")
parser.add_argument('--versions-file', default='versions.txt', help="File containing one URL per line.")
parser.add_argument('--access-key', required=True, help="AWS Access Key ID.")
parser.add_argument('--secret-key', required=True, help="AWS Secret Access Key.")
parser.add_argument('--endpoint-url', required=True, help="Custom endpoint URL for S3 or S3-compatible storage.")
args = parser.parse_args()
bucket_name = args.bucket
concurrency = args.concurrency
versions_file = args.versions_file
access_key = args.access_key
secret_key = args.secret_key
endpoint_url = args.endpoint_url
with open(versions_file, 'r') as f:
urls = [line.strip() for line in f if line.strip()]
print(f"Uploading {len(urls)} files...")
s3_params = {}
if access_key and secret_key:
s3_params['aws_access_key_id'] = access_key
s3_params['aws_secret_access_key'] = secret_key
if endpoint_url:
s3_params['endpoint_url'] = endpoint_url
s3 = boto3.client('s3', **s3_params)
futures = []
with ThreadPoolExecutor(max_workers=concurrency) as executor:
for url in urls:
object_key = derive_key_from_url(url)
futures.append(executor.submit(upload_url_to_s3, s3, bucket_name, url, object_key))
for future in as_completed(futures):
try:
uploaded_url, uploaded_key = future.result()
print(f"Uploaded: {uploaded_url}")
except Exception as e:
print(f"Error uploading: {e}")
if __name__ == "__main__":
main()

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++;
} }
return head;
} }
stdout.writeln("Launching game..."); Set<String> _toParameters(Command? parent) => parent?.parameters
var executable = version.shippingExecutable; .map((e) => '--${e.toLowerCase()}')
if(executable == null){ .toSet() ?? {};
throw Exception("Missing game executable at: ${version.location.path}");
}
final serverHost = result["server-host"]?.trim(); Map<String, Command> _toMap(List<Command> children) => Map.fromIterable(
if(serverHost?.isEmpty == true){ children,
throw Exception("Missing host name"); key: (command) => command.name.toLowerCase(),
} value: (command) => command
final serverPort = result["server-port"]?.trim();
if(serverPort?.isEmpty == true){
throw Exception("Missing port");
}
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,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);
}); });
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); if(!detached) {
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);
@@ -150,7 +212,8 @@ Future<void> _startAriaServer() async {
"--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" "--file-allocation=none",
"--check-certificate=false"
], ],
window: false window: false
); );

View File

@@ -7,11 +7,17 @@ import 'package:reboot_common/common.dart';
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();
@@ -25,7 +31,7 @@ Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
case InjectableDll.console: case InjectableDll.console:
name = "console.dll"; name = "console.dll";
case InjectableDll.auth: case InjectableDll.auth:
name = "starfall.dll"; name = "cobalt.dll";
case InjectableDll.memoryLeak: case InjectableDll.memoryLeak:
name = "memory.dll"; name = "memory.dll";
case InjectableDll.gameServer: case InjectableDll.gameServer:
@@ -45,12 +51,15 @@ Future<void> downloadDependency(InjectableDll dll, 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");

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.

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",

View File

@@ -35,10 +35,8 @@ class BackendController extends GetxController {
late final RxBool started; late final RxBool started;
late final RxBool detached; late final RxBool detached;
late final List<InfoBarEntry> _infoBars; late final List<InfoBarEntry> _infoBars;
StreamSubscription? worker; StreamSubscription? _worker;
int? embeddedProcessPid; ServerImplementation? _implementation;
HttpServer? localServer;
HttpServer? remoteServer;
BackendController() { BackendController() {
_storage = appWithNoStorage ? null : GetStorage(storageName); _storage = appWithNoStorage ? null : GetStorage(storageName);
@@ -48,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(() =>
@@ -148,18 +141,27 @@ class BackendController extends GetxController {
detached.value = false; detached.value = false;
} }
Future<bool> toggleInteractive() async { Future<bool> toggle() {
if(started.value) {
return stop(interactive: true);
}else {
return start(interactive: true);
}
}
Future<bool> start({required bool interactive}) async {
if(started.value) {
return true;
}
_cancel(); _cancel();
final stream = started.value ? stop() : start( final stream = startBackend(
onExit: () { type: type.value,
_cancel(); host: host.text,
_showRebootInfoBar( port: port.text,
translations.backendProcessError, detached: detached.value,
severity: InfoBarSeverity.error
);
},
onError: (errorMessage) { onError: (errorMessage) {
_cancel(); stop(interactive: false);
_showRebootInfoBar( _showRebootInfoBar(
translations.backendErrorMessage, translations.backendErrorMessage,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
@@ -173,266 +175,203 @@ class BackendController extends GetxController {
); );
final completer = Completer<bool>(); final completer = Completer<bool>();
InfoBarEntry? entry; InfoBarEntry? entry;
worker = stream.listen((event) { _worker = stream.listen((event) {
entry?.close(); entry?.close();
entry = _handeEvent(event); entry = _handeEvent(event, interactive);
if(event.type.isError) { if(event.type.isError) {
completer.complete(false); completer.complete(false);
}else if(event.type.isSuccess) { }else if(event.type.isSuccess) {
completer.complete(true); completer.complete(true);
} }
}); });
return await completer.future; return await completer.future;
} }
Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) async* { Future<bool> stop({required bool interactive}) async {
try {
if(started.value) {
return;
}
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 {
// If the local server is running on port 3551 there is no reverse proxy running
// We only need to check if everything is working
started.value = false;
}
break;
}
yield ServerResult(ServerResultType.pingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
remoteServer?.close(force: true);
localServer?.close(force: true);
started.value = false;
return;
}
yield ServerResult(ServerResultType.startSuccess);
}catch(error, stackTrace) {
yield ServerResult(
ServerResultType.startError,
error: error,
stackTrace: stackTrace
);
remoteServer?.close(force: true);
localServer?.close(force: true);
started.value = false;
}
}
Stream<ServerResult> stop() async* {
if(!started.value) { if(!started.value) {
return; return true;
} }
yield ServerResult(ServerResultType.stopping); _cancel();
started.value = false; final stream = stopBackend(
try{ type: type.value,
switch(type()){ implementation: _implementation
case ServerType.embedded: );
final embeddedProcessPid = this.embeddedProcessPid; final completer = Completer<bool>();
if(embeddedProcessPid != null) { InfoBarEntry? entry;
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm); _worker = stream.listen((event) {
this.embeddedProcessPid = null; entry?.close();
} entry = _handeEvent(event, interactive);
break; if(event.type.isError) {
case ServerType.remote: completer.complete(false);
await remoteServer?.close(force: true); }else if(event.type.isSuccess) {
remoteServer = null; completer.complete(true);
break;
case ServerType.local:
await localServer?.close(force: true);
localServer = null;
break;
} }
yield ServerResult(ServerResultType.stopSuccess); });
}catch(error, stackTrace){ return await completer.future;
yield ServerResult(
ServerResultType.stopError,
error: error,
stackTrace: stackTrace
);
started.value = true;
}
} }
void _cancel() { void _cancel() {
worker?.cancel(); // Do not await or it will hang _worker?.cancel(); // Do not await or it will hang
_infoBars.forEach((infoBar) => infoBar.close()); _infoBars.forEach((infoBar) => infoBar.close());
_infoBars.clear(); _infoBars.clear();
} }
InfoBarEntry _handeEvent(ServerResult event) { InfoBarEntry? _handeEvent(ServerResult event, bool interactive) {
log("[BACKEND] Handling event: $event"); 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) { switch (event.type) {
case ServerResultType.starting: case ServerResultType.starting:
return _showRebootInfoBar( if(interactive) {
translations.startingServer, return _showRebootInfoBar(
severity: InfoBarSeverity.info, translations.startingServer,
loading: true, severity: InfoBarSeverity.info,
duration: null loading: true,
); duration: null
);
}else {
return null;
}
case ServerResultType.startSuccess: case ServerResultType.startSuccess:
return _showRebootInfoBar( if(interactive) {
type.value == ServerType.local ? translations.checkedServer : translations.startedServer, return _showRebootInfoBar(
severity: InfoBarSeverity.success type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
); severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.startError: case ServerResultType.startError:
print(event.stackTrace); if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError), type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
}else {
return null;
}
case ServerResultType.stopping: case ServerResultType.stopping:
return _showRebootInfoBar( if(interactive) {
translations.stoppingServer, return _showRebootInfoBar(
severity: InfoBarSeverity.info, translations.stoppingServer,
loading: true, severity: InfoBarSeverity.info,
duration: null loading: true,
); duration: null
);
}else {
return null;
}
case ServerResultType.stopSuccess: case ServerResultType.stopSuccess:
return _showRebootInfoBar( if(interactive) {
translations.stoppedServer, return _showRebootInfoBar(
severity: InfoBarSeverity.success translations.stoppedServer,
); severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.stopError: case ServerResultType.stopError:
return _showRebootInfoBar( if(interactive) {
translations.stopServerError(event.error ?? translations.unknownError), return _showRebootInfoBar(
severity: InfoBarSeverity.error, translations.stopServerError(event.error ?? translations.unknownError),
duration: infoBarLongDuration severity: InfoBarSeverity.error,
); duration: infoBarLongDuration
case ServerResultType.missingHostError: );
return _showRebootInfoBar( }else {
translations.missingHostNameError, return null;
severity: InfoBarSeverity.error }
); case ServerResultType.startMissingHostError:
case ServerResultType.missingPortError: if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.missingPortError, translations.missingHostNameError,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.illegalPortError: }else {
return _showRebootInfoBar( return null;
translations.illegalPortError, }
severity: InfoBarSeverity.error case ServerResultType.startMissingPortError:
); if(interactive) {
case ServerResultType.freeingPort: return _showRebootInfoBar(
return _showRebootInfoBar( translations.missingPortError,
translations.freeingPort, severity: InfoBarSeverity.error
loading: true, );
duration: null }else {
); return null;
case ServerResultType.freePortSuccess: }
return _showRebootInfoBar( case ServerResultType.startIllegalPortError:
translations.freedPort, if(interactive) {
severity: InfoBarSeverity.success, return _showRebootInfoBar(
duration: infoBarShortDuration translations.illegalPortError,
); severity: InfoBarSeverity.error
case ServerResultType.freePortError: );
return _showRebootInfoBar( }else {
translations.freePortError(event.error ?? translations.unknownError), return null;
severity: InfoBarSeverity.error, }
duration: infoBarLongDuration case ServerResultType.startFreeingPort:
); if(interactive) {
case ServerResultType.pingingRemote: return _showRebootInfoBar(
return _showRebootInfoBar( translations.freeingPort,
translations.pingingServer(ServerType.remote.name), loading: true,
severity: InfoBarSeverity.info, duration: null
loading: true, );
duration: null }else {
); return null;
case ServerResultType.pingingLocal: }
return _showRebootInfoBar( case ServerResultType.startFreePortSuccess:
translations.pingingServer(type.value.name), if(interactive) {
severity: InfoBarSeverity.info, return _showRebootInfoBar(
loading: true, translations.freedPort,
duration: null severity: InfoBarSeverity.success,
); duration: infoBarShortDuration
case ServerResultType.pingError: );
return _showRebootInfoBar( }else {
translations.pingError(type.value.name), return null;
severity: InfoBarSeverity.error }
); 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;
} }
} }
@@ -598,4 +537,11 @@ class BackendController extends GetxController {
} }
return result; return result;
} }
Future<void> restart() async {
if(started.value) {
await stop(interactive: false);
await start(interactive: true);
}
}
} }

View File

@@ -16,8 +16,7 @@ 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 memoryLeakDll;
@@ -28,11 +27,10 @@ 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;
DllController() { DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName); _storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.gameServer); customGameServerDll = _createController("game_server", InjectableDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console); unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.auth); backendDll = _createController("backend", InjectableDll.auth);
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak); memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
@@ -59,7 +57,7 @@ class DllController extends GetxController {
} }
void resetGame() { void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.gameServer); customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console); unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.auth); backendDll.text = getDefaultDllPath(InjectableDll.auth);
} }
@@ -76,6 +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 {
InfoBarEntry? infoBarEntry;
try { try {
if(customGameServer.value) { if(customGameServer.value) {
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
@@ -101,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,7 +147,7 @@ class DllController extends GetxController {
switch(dll){ switch(dll){
case InjectableDll.gameServer: 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);
@@ -171,7 +170,7 @@ class DllController extends GetxController {
case InjectableDll.auth: case InjectableDll.auth:
return backendDll; return backendDll;
case InjectableDll.gameServer: case InjectableDll.gameServer:
return gameServerDll; return customGameServerDll;
case InjectableDll.memoryLeak: case InjectableDll.memoryLeak:
return memoryLeakDll; return memoryLeakDll;
} }
@@ -182,7 +181,7 @@ class DllController extends GetxController {
case InjectableDll.console: case InjectableDll.console:
return "${dllsDirectory.path}\\console.dll"; return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth: case InjectableDll.auth:
return "${dllsDirectory.path}\\starfall.dll"; return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer: case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll"; return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak: case InjectableDll.memoryLeak:

View File

@@ -110,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(
@@ -250,7 +250,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Generating instance args..."); log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs( final gameArgs = createRebootArgs(
host ? _hostingController.accountUsername.text : _gameController.username.text, host ? _hostingController.accountUsername.text : _gameController.username.text,
host ? _hostingController.accountPassword.text :_gameController.password.text, host ? _hostingController.accountPassword.text : _gameController.password.text,
host, host,
hostType, hostType,
false, false,
@@ -495,6 +495,7 @@ class _LaunchButtonState extends State<LaunchButton> {
const Duration(days: 1) 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.stop(); _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,

View File

@@ -35,18 +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 Future<List<FortniteBuild>> _fetchFuture;
SendPort? _downloadPort; SendPort? _downloadPort;
Object? _error; Object? _error;
StackTrace? _stackTrace; StackTrace? _stackTrace;
@override @override
void initState() { void initState() {
_fetchFuture = compute(fetchBuilds, null).then((value) {
_updateFormDefaults();
return value;
});
super.initState(); super.initState();
} }
@@ -60,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
@@ -68,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( return Obx(() => FormDialog(
future: _fetchFuture, content: _buildFormBody(downloadableBuilds),
builder: (context, snapshot) { buttons: _formButtons
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(
content: _buildFormBody(data),
buttons: _formButtons
));
}
);
case _DownloadStatus.downloading: case _DownloadStatus.downloading:
case _DownloadStatus.extracting: case _DownloadStatus.extracting:
return GenericDialog( return GenericDialog(
@@ -256,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,
@@ -450,7 +427,6 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return; return;
} }
print("${bestDisk.path}\\FortniteBuilds\\${build.version}");
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}"; final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}";
_pathController.text = pathText; _pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length); _pathController.selection = TextSelection.collapsed(offset: pathText.length);

View File

@@ -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

@@ -75,6 +75,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
lastPage = index; lastPage = index;
_pageController.jumpToPage(index); _pageController.jumpToPage(index);
pagesController.add(null);
}); });
} }
@@ -152,7 +153,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
try { try {
if(_backendController.started.value) { if(_backendController.started.value) {
await _backendController.toggleInteractive(); await _backendController.toggle();
} }
}catch(error) { }catch(error) {
log("[BACKEND] Cannot stop backend on exit: $error"); log("[BACKEND] Cannot stop backend on exit: $error");
@@ -524,36 +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),
)
);
Widget get _autoSuggestBox => Padding( Widget get _autoSuggestBox => Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,

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';
@@ -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;
@@ -115,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
@@ -141,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),
@@ -184,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.gameServer); final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
_dllController.gameServerDll.text = path; _dllController.customGameServerDll.text = path;
_dllController.download(InjectableDll.gameServer, 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(
@@ -209,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),
@@ -273,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

@@ -45,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

@@ -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 { await _controller.stop(interactive: false);
_controller.stop(); _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,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Graphical User Interface for Project Reboot description: Graphical User Interface for Project Reboot
version: "10.0.5" version: "10.0.7"
publish_to: 'none' publish_to: 'none'
@@ -43,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