4 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
24 changed files with 1395 additions and 671 deletions

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; class Command {
} final String name;
final List<String> parameters;
writeMatchmakingIp(result["matchmaking-address"]); final List<Command> subCommands;
autoRestart = result["auto-restart"];
await startGame(); 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

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

@@ -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();
@@ -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) {
@@ -292,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.

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,265 +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;
if(embeddedProcessPid != null) {
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; 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() { 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:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.startingServer, translations.startingServer,
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
}else {
return null;
}
case ServerResultType.startSuccess: case ServerResultType.startSuccess:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer, type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
); );
}else {
return null;
}
case ServerResultType.startError: case ServerResultType.startError:
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:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.stoppingServer, translations.stoppingServer,
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
}else {
return null;
}
case ServerResultType.stopSuccess: case ServerResultType.stopSuccess:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.stoppedServer, translations.stoppedServer,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
); );
}else {
return null;
}
case ServerResultType.stopError: case ServerResultType.stopError:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError), translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
case ServerResultType.missingHostError: }else {
return null;
}
case ServerResultType.startMissingHostError:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.missingHostNameError, translations.missingHostNameError,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.missingPortError: }else {
return null;
}
case ServerResultType.startMissingPortError:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.missingPortError, translations.missingPortError,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.illegalPortError: }else {
return null;
}
case ServerResultType.startIllegalPortError:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.illegalPortError, translations.illegalPortError,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.freeingPort: }else {
return null;
}
case ServerResultType.startFreeingPort:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.freeingPort, translations.freeingPort,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.freePortSuccess: }else {
return null;
}
case ServerResultType.startFreePortSuccess:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.freedPort, translations.freedPort,
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
); );
case ServerResultType.freePortError: }else {
return null;
}
case ServerResultType.startFreePortError:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError), translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
case ServerResultType.pingingRemote: }else {
return null;
}
case ServerResultType.startPingingRemote:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name), translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.pingingLocal: }else {
return null;
}
case ServerResultType.startPingingLocal:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.pingingServer(type.value.name), translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.pingError: }else {
return null;
}
case ServerResultType.startPingError:
if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
translations.pingError(type.value.name), translations.pingError(type.value.name),
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
}else {
return null;
}
case ServerResultType.startedImplementation:
_implementation = event.implementation;
return null;
} }
} }
@@ -597,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

@@ -27,7 +27,6 @@ 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);
@@ -75,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;
@@ -100,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

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(
@@ -526,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;
@@ -629,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

@@ -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),
@@ -194,6 +193,24 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
} }
}); });
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 {
_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,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.6" 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