mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90448eeaa1 | ||
|
|
b319479def |
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
156
cli/lib/cli.dart
156
cli/lib/cli.dart
@@ -1,87 +1,89 @@
|
||||
import 'dart:io';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:reboot_cli/src/game.dart';
|
||||
import 'package:reboot_cli/src/reboot.dart';
|
||||
import 'package:reboot_cli/src/server.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
class Parser {
|
||||
final List<Command> commands;
|
||||
|
||||
late String? username;
|
||||
late bool host;
|
||||
late bool verbose;
|
||||
late String dll;
|
||||
late FortniteVersion version;
|
||||
late bool autoRestart;
|
||||
Parser({required this.commands});
|
||||
|
||||
void main(List<String> args) async {
|
||||
stdout.writeln("Reboot Launcher");
|
||||
stdout.writeln("Wrote by Auties00");
|
||||
stdout.writeln("Version 1.0");
|
||||
|
||||
kill();
|
||||
|
||||
var parser = ArgParser()
|
||||
..addOption("path", mandatory: true)
|
||||
..addOption("username")
|
||||
..addOption("server-type", allowed: ServerType.values.map((entry) => entry.name), defaultsTo: ServerType.embedded.name)
|
||||
..addOption("server-host")
|
||||
..addOption("server-port")
|
||||
..addOption("matchmaking-address")
|
||||
..addOption("dll", defaultsTo: rebootDllFile.path)
|
||||
..addFlag("update", defaultsTo: true, negatable: true)
|
||||
..addFlag("log", defaultsTo: false)
|
||||
..addFlag("host", defaultsTo: false)
|
||||
..addFlag("auto-restart", defaultsTo: false, negatable: true);
|
||||
var result = parser.parse(args);
|
||||
|
||||
dll = result["dll"];
|
||||
host = result["host"];
|
||||
username = result["username"] ?? kDefaultPlayerName;
|
||||
verbose = result["log"];
|
||||
version = FortniteVersion(name: "Dummy", location: Directory(result["path"]));
|
||||
|
||||
await downloadRequiredDLLs();
|
||||
if(result["update"]) {
|
||||
stdout.writeln("Updating reboot dll...");
|
||||
try {
|
||||
await downloadRebootDll(kRebootDownloadUrl);
|
||||
}catch(error){
|
||||
stderr.writeln("Cannot update reboot dll: $error");
|
||||
CommandCall? parse(List<String> args) {
|
||||
var position = 0;
|
||||
var allowedCommands = _toMap(commands);
|
||||
var allowedParameters = <String>{};
|
||||
Command? command;
|
||||
CommandCall? head;
|
||||
CommandCall? tail;
|
||||
String? parameterName;
|
||||
while(position < args.length) {
|
||||
final current = args[position].toLowerCase();
|
||||
if(parameterName != null) {
|
||||
tail?.parameters[parameterName] = current;
|
||||
parameterName = null;
|
||||
}else if(allowedParameters.contains(current.toLowerCase())) {
|
||||
parameterName = current.substring(2);
|
||||
if(args.elementAtOrNull(position + 1) == '"') {
|
||||
position++;
|
||||
}
|
||||
}else {
|
||||
final newCommand = allowedCommands[current];
|
||||
if(newCommand != null) {
|
||||
final newCall = CommandCall(name: newCommand.name);
|
||||
if(head == null) {
|
||||
head = newCall;
|
||||
tail = newCall;
|
||||
}
|
||||
if(tail != null) {
|
||||
tail.subCall = newCall;
|
||||
}
|
||||
tail = newCall;
|
||||
command = newCommand;
|
||||
allowedCommands = _toMap(newCommand.subCommands);
|
||||
allowedParameters = _toParameters(command);
|
||||
}
|
||||
}
|
||||
position++;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
stdout.writeln("Launching game...");
|
||||
var executable = version.shippingExecutable;
|
||||
if(executable == null){
|
||||
throw Exception("Missing game executable at: ${version.location.path}");
|
||||
}
|
||||
Set<String> _toParameters(Command? parent) => parent?.parameters
|
||||
.map((e) => '--${e.toLowerCase()}')
|
||||
.toSet() ?? {};
|
||||
|
||||
final serverHost = result["server-host"]?.trim();
|
||||
if(serverHost?.isEmpty == true){
|
||||
throw Exception("Missing host name");
|
||||
}
|
||||
|
||||
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"])
|
||||
Map<String, Command> _toMap(List<Command> children) => Map.fromIterable(
|
||||
children,
|
||||
key: (command) => command.name.toLowerCase(),
|
||||
value: (command) => command
|
||||
);
|
||||
if(!started){
|
||||
stderr.writeln("Cannot start server!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
writeMatchmakingIp(result["matchmaking-address"]);
|
||||
autoRestart = result["auto-restart"];
|
||||
await startGame();
|
||||
class Command {
|
||||
final String name;
|
||||
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
102
cli/lib/main.dart
Normal 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());
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(_){
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
name: reboot_cli
|
||||
description: Command Line Interface for Project Reboot
|
||||
version: "1.0.0"
|
||||
version: "10.0.7"
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <=3.3.4"
|
||||
sdk: ">=2.19.0 <=3.5.3"
|
||||
|
||||
dependencies:
|
||||
reboot_common:
|
||||
path: ./../common
|
||||
args: ^2.3.1
|
||||
process_run: ^0.13.1
|
||||
tint: ^2.0.1
|
||||
interact: ^2.2.0
|
||||
args: ^2.6.0
|
||||
|
||||
dependency_overrides:
|
||||
xml: ^6.3.0
|
||||
|
||||
@@ -22,4 +22,5 @@ const List<String> kCannotConnectErrors = [
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
const String kGameFinishedLine = "PlayersLeft: 1";
|
||||
const String kDisplayInitializedLine = "Display";
|
||||
const String kDisplayLine = "Display";
|
||||
const String kDisplayInitializedLine = "Initialized";
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
class ServerResult {
|
||||
final ServerResultType type;
|
||||
final ServerImplementation? implementation;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
ServerResult(this.type, {this.error, this.stackTrace});
|
||||
ServerResult(this.type, {this.implementation, this.error, this.stackTrace});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -11,22 +14,32 @@ class ServerResult {
|
||||
}
|
||||
}
|
||||
|
||||
class ServerImplementation {
|
||||
final Process? process;
|
||||
final HttpServer? server;
|
||||
|
||||
ServerImplementation({this.process, this.server});
|
||||
}
|
||||
|
||||
enum ServerResultType {
|
||||
starting,
|
||||
startMissingHostError,
|
||||
startMissingPortError,
|
||||
startIllegalPortError,
|
||||
startFreeingPort,
|
||||
startFreePortSuccess,
|
||||
startFreePortError,
|
||||
startPingingRemote,
|
||||
startPingingLocal,
|
||||
startPingError,
|
||||
startedImplementation,
|
||||
startSuccess,
|
||||
startError,
|
||||
stopping,
|
||||
stopSuccess,
|
||||
stopError,
|
||||
missingHostError,
|
||||
missingPortError,
|
||||
illegalPortError,
|
||||
freeingPort,
|
||||
freePortSuccess,
|
||||
freePortError,
|
||||
pingingRemote,
|
||||
pingingLocal,
|
||||
pingError;
|
||||
stopError;
|
||||
|
||||
bool get isStart => name.contains("start");
|
||||
|
||||
bool get isError => name.contains("Error");
|
||||
|
||||
|
||||
@@ -15,6 +15,122 @@ final Semaphore _semaphore = Semaphore();
|
||||
String? _lastIp;
|
||||
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 {
|
||||
final process = await startProcess(
|
||||
executable: backendStartExecutable,
|
||||
@@ -25,7 +141,9 @@ Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onEr
|
||||
log("[BACKEND] Error: $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;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,37 +13,99 @@ import 'package:http/http.dart' as http;
|
||||
|
||||
const String kStopBuildDownloadSignal = "kill";
|
||||
|
||||
final Uri _archiveSourceUrl = Uri.parse("https://builds.rebootfn.org/versions.json");
|
||||
final int _ariaPort = 6800;
|
||||
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
|
||||
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
|
||||
final String _ariaSecret = "RebootLauncher";
|
||||
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 {
|
||||
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
||||
@@ -150,7 +212,8 @@ Future<void> _startAriaServer() async {
|
||||
"--rpc-allow-origin-all",
|
||||
"--rpc-secret=$_ariaSecret",
|
||||
"--rpc-listen-port=$_ariaPort",
|
||||
"--file-allocation=none"
|
||||
"--file-allocation=none",
|
||||
"--check-certificate=false"
|
||||
],
|
||||
window: false
|
||||
);
|
||||
|
||||
@@ -7,11 +7,17 @@ import 'package:reboot_common/common.dart';
|
||||
|
||||
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
|
||||
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
|
||||
|
||||
const String kRebootBelowS20DownloadUrl =
|
||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
|
||||
const String kRebootAboveS20DownloadUrl =
|
||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
|
||||
|
||||
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 {
|
||||
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
|
||||
@@ -25,7 +31,7 @@ Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
|
||||
case InjectableDll.console:
|
||||
name = "console.dll";
|
||||
case InjectableDll.auth:
|
||||
name = "starfall.dll";
|
||||
name = "cobalt.dll";
|
||||
case InjectableDll.memoryLeak:
|
||||
name = "memory.dll";
|
||||
case InjectableDll.gameServer:
|
||||
@@ -45,12 +51,15 @@ Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
|
||||
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;
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
var response = await http.get(Uri.parse(url));
|
||||
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");
|
||||
|
||||
@@ -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) {
|
||||
log("[PROCESS] Generating reboot args");
|
||||
if(password.isEmpty) {
|
||||
@@ -264,17 +250,13 @@ void handleGameOutput({
|
||||
}else if(line.contains(kGameFinishedLine) && host) {
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
|
||||
onMatchEnd();
|
||||
}else if(line.contains(kDisplayInitializedLine) && host) {
|
||||
}else if(line.contains(kDisplayLine) && line.contains(kDisplayInitializedLine) && host) {
|
||||
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
|
||||
onDisplayAttached();
|
||||
}
|
||||
}
|
||||
|
||||
String _parseUsername(String username, bool host) {
|
||||
if(host) {
|
||||
return "Player${Random().nextInt(1000)}";
|
||||
}
|
||||
|
||||
if (username.isEmpty) {
|
||||
return kDefaultPlayerName;
|
||||
}
|
||||
@@ -296,16 +278,8 @@ final class _ExtendedProcess implements Process {
|
||||
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
|
||||
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
|
||||
|
||||
|
||||
@override
|
||||
Future<int> get exitCode {
|
||||
try {
|
||||
return _delegate.exitCode;
|
||||
}catch(_) {
|
||||
return watchProcess(_delegate.pid)
|
||||
.then((_) => -1);
|
||||
}
|
||||
}
|
||||
Future<int> get exitCode => _delegate.exitCode;
|
||||
|
||||
@override
|
||||
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
|
||||
|
||||
BIN
gui/dependencies/dlls/RebootFallback.zip
Normal file
BIN
gui/dependencies/dlls/RebootFallback.zip
Normal file
Binary file not shown.
BIN
gui/dependencies/dlls/RebootS20Fallback.zip
Normal file
BIN
gui/dependencies/dlls/RebootS20Fallback.zip
Normal file
Binary file not shown.
BIN
gui/dependencies/dlls/cobalt.dll
Normal file
BIN
gui/dependencies/dlls/cobalt.dll
Normal file
Binary file not shown.
@@ -128,7 +128,7 @@
|
||||
"importVersionDescription": "Import a new version of Fortnite into the launcher",
|
||||
"addLocalBuildName": "Add a version from this PC's local storage",
|
||||
"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",
|
||||
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
|
||||
"downloadBuildContent": "Download build",
|
||||
|
||||
@@ -35,10 +35,8 @@ class BackendController extends GetxController {
|
||||
late final RxBool started;
|
||||
late final RxBool detached;
|
||||
late final List<InfoBarEntry> _infoBars;
|
||||
StreamSubscription? worker;
|
||||
int? embeddedProcessPid;
|
||||
HttpServer? localServer;
|
||||
HttpServer? remoteServer;
|
||||
StreamSubscription? _worker;
|
||||
ServerImplementation? _implementation;
|
||||
|
||||
BackendController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
@@ -48,11 +46,6 @@ class BackendController extends GetxController {
|
||||
host.text = _readHost();
|
||||
port.text = _readPort();
|
||||
_storage?.write("type", value.index);
|
||||
if (!started.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
stop();
|
||||
});
|
||||
host = TextEditingController(text: _readHost());
|
||||
host.addListener(() =>
|
||||
@@ -148,18 +141,27 @@ class BackendController extends GetxController {
|
||||
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();
|
||||
final stream = started.value ? stop() : start(
|
||||
onExit: () {
|
||||
_cancel();
|
||||
_showRebootInfoBar(
|
||||
translations.backendProcessError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
},
|
||||
final stream = startBackend(
|
||||
type: type.value,
|
||||
host: host.text,
|
||||
port: port.text,
|
||||
detached: detached.value,
|
||||
onError: (errorMessage) {
|
||||
_cancel();
|
||||
stop(interactive: false);
|
||||
_showRebootInfoBar(
|
||||
translations.backendErrorMessage,
|
||||
severity: InfoBarSeverity.error,
|
||||
@@ -173,266 +175,203 @@ class BackendController extends GetxController {
|
||||
);
|
||||
final completer = Completer<bool>();
|
||||
InfoBarEntry? entry;
|
||||
worker = stream.listen((event) {
|
||||
_worker = stream.listen((event) {
|
||||
entry?.close();
|
||||
entry = _handeEvent(event);
|
||||
entry = _handeEvent(event, interactive);
|
||||
if(event.type.isError) {
|
||||
completer.complete(false);
|
||||
}else if(event.type.isSuccess) {
|
||||
completer.complete(true);
|
||||
}
|
||||
});
|
||||
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) 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* {
|
||||
Future<bool> stop({required bool interactive}) async {
|
||||
if(!started.value) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
yield ServerResult(ServerResultType.stopping);
|
||||
started.value = false;
|
||||
try{
|
||||
switch(type()){
|
||||
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;
|
||||
_cancel();
|
||||
final stream = stopBackend(
|
||||
type: type.value,
|
||||
implementation: _implementation
|
||||
);
|
||||
final completer = Completer<bool>();
|
||||
InfoBarEntry? entry;
|
||||
_worker = stream.listen((event) {
|
||||
entry?.close();
|
||||
entry = _handeEvent(event, interactive);
|
||||
if(event.type.isError) {
|
||||
completer.complete(false);
|
||||
}else if(event.type.isSuccess) {
|
||||
completer.complete(true);
|
||||
}
|
||||
yield ServerResult(ServerResultType.stopSuccess);
|
||||
}catch(error, stackTrace){
|
||||
yield ServerResult(
|
||||
ServerResultType.stopError,
|
||||
error: error,
|
||||
stackTrace: stackTrace
|
||||
);
|
||||
started.value = true;
|
||||
}
|
||||
});
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
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.clear();
|
||||
}
|
||||
|
||||
InfoBarEntry _handeEvent(ServerResult event) {
|
||||
log("[BACKEND] Handling event: $event");
|
||||
InfoBarEntry? _handeEvent(ServerResult event, bool interactive) {
|
||||
log("[BACKEND] Handling event: $event (interactive: $interactive, start: ${event.type.isStart}, error: ${event.type.isError})");
|
||||
started.value = event.type.isStart && !event.type.isError;
|
||||
switch (event.type) {
|
||||
case ServerResultType.starting:
|
||||
return _showRebootInfoBar(
|
||||
translations.startingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.startingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startSuccess:
|
||||
return _showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startError:
|
||||
print(event.stackTrace);
|
||||
return _showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.stopping:
|
||||
return _showRebootInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.stoppingServer,
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.stopSuccess:
|
||||
return _showRebootInfoBar(
|
||||
translations.stoppedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.stoppedServer,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.stopError:
|
||||
return _showRebootInfoBar(
|
||||
translations.stopServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.missingHostError:
|
||||
return _showRebootInfoBar(
|
||||
translations.missingHostNameError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.missingPortError:
|
||||
return _showRebootInfoBar(
|
||||
translations.missingPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.illegalPortError:
|
||||
return _showRebootInfoBar(
|
||||
translations.illegalPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
case ServerResultType.freeingPort:
|
||||
return _showRebootInfoBar(
|
||||
translations.freeingPort,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.freePortSuccess:
|
||||
return _showRebootInfoBar(
|
||||
translations.freedPort,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
case ServerResultType.freePortError:
|
||||
return _showRebootInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
case ServerResultType.pingingRemote:
|
||||
return _showRebootInfoBar(
|
||||
translations.pingingServer(ServerType.remote.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingingLocal:
|
||||
return _showRebootInfoBar(
|
||||
translations.pingingServer(type.value.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
case ServerResultType.pingError:
|
||||
return _showRebootInfoBar(
|
||||
translations.pingError(type.value.name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.stopServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startMissingHostError:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.missingHostNameError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startMissingPortError:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.missingPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startIllegalPortError:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.illegalPortError,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startFreeingPort:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.freeingPort,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startFreePortSuccess:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.freedPort,
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startFreePortError:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.freePortError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startPingingRemote:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.pingingServer(ServerType.remote.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startPingingLocal:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.pingingServer(type.value.name),
|
||||
severity: InfoBarSeverity.info,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startPingError:
|
||||
if(interactive) {
|
||||
return _showRebootInfoBar(
|
||||
translations.pingError(type.value.name),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
case ServerResultType.startedImplementation:
|
||||
_implementation = event.implementation;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,4 +537,11 @@ class BackendController extends GetxController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> restart() async {
|
||||
if(started.value) {
|
||||
await stop(interactive: false);
|
||||
await start(interactive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,7 @@ class DllController extends GetxController {
|
||||
static const String storageName = "v2_dll_storage";
|
||||
|
||||
late final GetStorage? _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController gameServerDll;
|
||||
late final TextEditingController customGameServerDll;
|
||||
late final TextEditingController unrealEngineConsoleDll;
|
||||
late final TextEditingController backendDll;
|
||||
late final TextEditingController memoryLeakDll;
|
||||
@@ -28,11 +27,10 @@ class DllController extends GetxController {
|
||||
late final RxBool customGameServer;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
InfoBarEntry? infoBarEntry;
|
||||
|
||||
DllController() {
|
||||
_storage = appWithNoStorage ? null : GetStorage(storageName);
|
||||
gameServerDll = _createController("game_server", InjectableDll.gameServer);
|
||||
customGameServerDll = _createController("game_server", InjectableDll.gameServer);
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
|
||||
backendDll = _createController("backend", InjectableDll.auth);
|
||||
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
|
||||
@@ -59,7 +57,7 @@ class DllController extends GetxController {
|
||||
}
|
||||
|
||||
void resetGame() {
|
||||
gameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
|
||||
customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
|
||||
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
|
||||
backendDll.text = getDefaultDllPath(InjectableDll.auth);
|
||||
}
|
||||
@@ -76,6 +74,7 @@ class DllController extends GetxController {
|
||||
}
|
||||
|
||||
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
|
||||
InfoBarEntry? infoBarEntry;
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
@@ -101,8 +100,8 @@ class DllController extends GetxController {
|
||||
}
|
||||
await Future.wait(
|
||||
[
|
||||
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text),
|
||||
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text),
|
||||
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text, false),
|
||||
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text, true),
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
],
|
||||
eagerError: false
|
||||
@@ -148,7 +147,7 @@ class DllController extends GetxController {
|
||||
switch(dll){
|
||||
case InjectableDll.gameServer:
|
||||
if(customGameServer.value) {
|
||||
return (File(gameServerDll.text), true);
|
||||
return (File(customGameServerDll.text), true);
|
||||
}
|
||||
|
||||
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
|
||||
@@ -171,7 +170,7 @@ class DllController extends GetxController {
|
||||
case InjectableDll.auth:
|
||||
return backendDll;
|
||||
case InjectableDll.gameServer:
|
||||
return gameServerDll;
|
||||
return customGameServerDll;
|
||||
case InjectableDll.memoryLeak:
|
||||
return memoryLeakDll;
|
||||
}
|
||||
@@ -182,7 +181,7 @@ class DllController extends GetxController {
|
||||
case InjectableDll.console:
|
||||
return "${dllsDirectory.path}\\console.dll";
|
||||
case InjectableDll.auth:
|
||||
return "${dllsDirectory.path}\\starfall.dll";
|
||||
return "${dllsDirectory.path}\\cobalt.dll";
|
||||
case InjectableDll.gameServer:
|
||||
return "${dllsDirectory.path}\\reboot.dll";
|
||||
case InjectableDll.memoryLeak:
|
||||
|
||||
@@ -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})...");
|
||||
final backendResult = _backendController.started() || await _backendController.toggleInteractive();
|
||||
final backendResult = _backendController.started() || await _backendController.toggle();
|
||||
if(!backendResult){
|
||||
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
|
||||
_onStop(
|
||||
@@ -250,7 +250,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
||||
final gameArgs = createRebootArgs(
|
||||
host ? _hostingController.accountUsername.text : _gameController.username.text,
|
||||
host ? _hostingController.accountPassword.text :_gameController.password.text,
|
||||
host ? _hostingController.accountPassword.text : _gameController.password.text,
|
||||
host,
|
||||
hostType,
|
||||
false,
|
||||
@@ -495,6 +495,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
const Duration(days: 1)
|
||||
);
|
||||
this._pingOperation = pingOperation;
|
||||
_gameServerInfoBar?.close();
|
||||
_gameServerInfoBar = showRebootInfoBar(
|
||||
translations.checkGameServerFixMessage(gameServerPort),
|
||||
action: Button(
|
||||
@@ -508,8 +509,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final result = await pingOperation.future;
|
||||
_gameServerInfoBar?.close();
|
||||
return result;
|
||||
}finally {
|
||||
}catch(_) {
|
||||
_gameServerInfoBar?.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,7 +526,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
_backendController.stop();
|
||||
_backendController.stop(interactive: false);
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
@@ -627,7 +629,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
);
|
||||
break;
|
||||
case _StopReason.tokenError:
|
||||
_backendController.stop();
|
||||
_backendController.stop(interactive: false);
|
||||
showRebootInfoBar(
|
||||
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
|
||||
severity: InfoBarSeverity.error,
|
||||
|
||||
@@ -35,18 +35,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
final Rxn<double> _progress = Rxn();
|
||||
final RxInt _speed = RxInt(0);
|
||||
|
||||
late Future<List<FortniteBuild>> _fetchFuture;
|
||||
|
||||
SendPort? _downloadPort;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchFuture = compute(fetchBuilds, null).then((value) {
|
||||
_updateFormDefaults();
|
||||
return value;
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -60,6 +54,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
void _cancelDownload() {
|
||||
_downloadPort?.send(kStopBuildDownloadSignal);
|
||||
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
|
||||
stopDownloadServer();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -68,28 +63,10 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
child: Obx(() {
|
||||
switch(_status.value){
|
||||
case _DownloadStatus.form:
|
||||
return FutureBuilder(
|
||||
future: _fetchFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
final data = snapshot.data;
|
||||
if (data == null) {
|
||||
return ProgressDialog(
|
||||
text: translations.fetchingBuilds,
|
||||
showButton: widget.closable,
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return Obx(() => FormDialog(
|
||||
content: _buildFormBody(data),
|
||||
buttons: _formButtons
|
||||
));
|
||||
}
|
||||
);
|
||||
return Obx(() => FormDialog(
|
||||
content: _buildFormBody(downloadableBuilds),
|
||||
buttons: _formButtons
|
||||
));
|
||||
case _DownloadStatus.downloading:
|
||||
case _DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
@@ -256,7 +233,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(timeLeft != null)
|
||||
if(timeLeft != null && timeLeft != -1)
|
||||
Text(
|
||||
translations.timeLeft(timeLeft),
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
@@ -450,7 +427,6 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
print("${bestDisk.path}\\FortniteBuilds\\${build.version}");
|
||||
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}";
|
||||
_pathController.text = pathText;
|
||||
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
|
||||
|
||||
@@ -162,7 +162,12 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
key: backendDetachedOverlayTargetKey,
|
||||
child: ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
onChanged: (value) async {
|
||||
_backendController.detached.value = value;
|
||||
if(_backendController.started.value) {
|
||||
await _backendController.restart();
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -75,6 +75,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
lastPage = index;
|
||||
_pageController.jumpToPage(index);
|
||||
pagesController.add(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,7 +153,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
try {
|
||||
if(_backendController.started.value) {
|
||||
await _backendController.toggleInteractive();
|
||||
await _backendController.toggle();
|
||||
}
|
||||
}catch(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(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||
@@ -36,6 +39,7 @@ class SettingsPage extends RebootPage {
|
||||
class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final DllController _dllController = Get.find<DllController>();
|
||||
int? _downloadFromMirrorId;
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
@@ -115,7 +119,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
}
|
||||
|
||||
_dllController.customGameServer.value = entry.key;
|
||||
_dllController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_dllController.updateGameServerDll(
|
||||
force: true
|
||||
@@ -141,11 +144,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
child: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.beforeS20Mirror,
|
||||
onChanged: (value) {
|
||||
if(Uri.tryParse(value) != null) {
|
||||
_dllController.updateGameServerDll(force: true);
|
||||
}
|
||||
},
|
||||
onChanged: _scheduleMirrorDownload
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
@@ -184,16 +183,34 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
return createFileSetting(
|
||||
title: translations.settingsOldServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _dllController.gameServerDll,
|
||||
controller: _dllController.customGameServerDll,
|
||||
onReset: () {
|
||||
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
|
||||
_dllController.gameServerDll.text = path;
|
||||
_dllController.customGameServerDll.text = 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(() {
|
||||
if(!_dllController.customGameServer.value) {
|
||||
return SettingTile(
|
||||
@@ -209,11 +226,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
child: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _dllController.aboveS20Mirror,
|
||||
onChanged: (value) {
|
||||
if(Uri.tryParse(value) != null) {
|
||||
_dllController.updateGameServerDll(force: true);
|
||||
}
|
||||
},
|
||||
onChanged: _scheduleMirrorDownload
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
@@ -273,7 +286,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_dllController.timer.value = entry;
|
||||
_dllController.infoBarEntry?.close();
|
||||
_dllController.updateGameServerDll(
|
||||
force: true
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ class _ServerButtonState extends State<ServerButton> {
|
||||
builder: (context, snapshot) => Obx(() => Text(_buttonText))
|
||||
),
|
||||
),
|
||||
onPressed: () => _controller.toggleInteractive()
|
||||
onPressed: () => _controller.toggle()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -32,18 +32,16 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||
));
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createItem(ServerType type) {
|
||||
return MenuFlyoutItem(
|
||||
text: Text(type.label),
|
||||
onPressed: () async {
|
||||
_controller.stop();
|
||||
_controller.type.value = type;
|
||||
}
|
||||
);
|
||||
}
|
||||
MenuFlyoutItem _createItem(ServerType type) => MenuFlyoutItem(
|
||||
text: Text(type.label),
|
||||
onPressed: () async {
|
||||
await _controller.stop(interactive: false);
|
||||
_controller.type.value = type;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
extension ServerTypeExtension on ServerType {
|
||||
extension _ServerTypeExtension on ServerType {
|
||||
String get label {
|
||||
return this == ServerType.embedded ? translations.embedded
|
||||
: this == ServerType.remote ? translations.remote
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: reboot_launcher
|
||||
description: Graphical User Interface for Project Reboot
|
||||
version: "10.0.5"
|
||||
version: "10.0.7"
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
@@ -43,6 +43,7 @@ dependencies:
|
||||
# Async helpers
|
||||
async: ^2.11.0
|
||||
sync: ^0.3.0
|
||||
synchronized: ^3.3.0+3
|
||||
|
||||
# State management
|
||||
get: ^4.6.6
|
||||
|
||||
Reference in New Issue
Block a user