12 Commits

Author SHA1 Message Date
Alessandro Autiero
9c6cd6dd37 Merge remote-tracking branch 'origin/master' 2025-04-16 15:43:43 +02:00
Alessandro Autiero
c3ede3b745 10.0.9 2025-04-16 15:43:34 +02:00
Alessandro Autiero
d2f0d176eb Update PortForwarding.md 2025-04-08 18:36:09 +02:00
Alessandro Autiero
f9cf99a6b2 Update README.md 2025-03-24 20:50:43 +01:00
Alessandro Autiero
dc2d4c4377 10.0.8 2025-03-23 23:17:20 +01:00
Alessandro Autiero
5d8f6bf0fa 10.0.8 2025-03-23 20:26:13 +01:00
Alessandro Autiero
9a000db3b7 10.0.8 2025-03-23 18:25:47 +01:00
Alessandro Autiero
4327541ac6 Merge pull request #257 from Milxnor/master
Added dedicated_server endpoints & Update keychain
2025-03-23 16:33:39 +01:00
Gray
64dc971da4 Added dedicated_server endpoints 2025-03-22 07:47:46 -04:00
Gray
d36da909ed Update keychain (for events and new cosmetics) 2025-03-22 07:47:25 -04:00
Alessandro Autiero
90448eeaa1 10.0.7 2025-03-08 17:06:01 +01:00
Alessandro Autiero
b319479def 10.0.6 2025-02-04 13:50:01 +01:00
89 changed files with 7337 additions and 4504 deletions

View File

@@ -1,16 +1,25 @@
![Banner](https://i.imgur.com/p0P4tcI.png) ![Banner](https://i.imgur.com/p0P4tcI.png)
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/) GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
Join our [Discord](https://discord.gg/rebootmp) Join our [Discord](https://discord.gg/rebootmp)
Install the launcher easily from the [releases](https://github.com/Auties00/Reboot-Launcher/releases/) section
## Modules ## Modules
- COMMON: Shared business logic for CLI and GUI modules - COMMON: Shared business logic for CLI and GUI modules
- CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart - CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart
- GUI: Stable graphical user interface to play and host Fortnite S0-14 - GUI: Stable graphical user interface to play and host Fortnite S0-14
![image](https://github.com/user-attachments/assets/7ff5d49e-8920-41ad-a805-188d84ad6ec4)
## Preview
## Installation - GUI
Check the releases section ![Registrazione 2025-03-24 194421](https://github.com/user-attachments/assets/f3452969-76ba-49e7-b707-42754bacad70)
- CLI
Coming soon!

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,87 +0,0 @@
import 'dart:io';
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';
late String? username;
late bool host;
late bool verbose;
late String dll;
late FortniteVersion version;
late bool autoRestart;
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");
}
}
stdout.writeln("Launching game...");
var executable = version.shippingExecutable;
if(executable == null){
throw Exception("Missing game executable at: ${version.location.path}");
}
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"])
);
if(!started){
stderr.writeln("Cannot start server!");
return;
}
writeMatchmakingIp(result["matchmaking-address"]);
autoRestart = result["auto-restart"];
await startGame();
}

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

@@ -0,0 +1,437 @@
import 'dart:io';
import 'dart:isolate';
import 'package:interact_cli/interact_cli.dart';
import 'package:reboot_cli/src/controller/config.dart';
import 'package:reboot_cli/src/util/console.dart';
import 'package:reboot_cli/src/util/extensions.dart';
import 'package:reboot_common/common.dart';
import 'package:tint/tint.dart';
import 'package:version/version.dart';
const Command _buildList = Command(name: 'list', parameters: [], subCommands: []);
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: 'versions', parameters: [], subCommands: [_buildList, _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: []);
final List<String> _versions = downloadableBuilds.map((build) => build.gameVersion).toList(growable: false);
const String _playVersionAction = 'Play';
const String _hostVersionAction = 'Host';
const String _deleteVersionAction = 'Delete';
const String _infoVersionAction = 'Info';
const List<String> _versionActions = [_playVersionAction, _hostVersionAction, _deleteVersionAction, _infoVersionAction];
void main(List<String> args) async {
enableLoggingToConsole = false;
useDefaultPath = true;
print("""
🎮 Reboot Launcher
🔥 Launch, manage, and play Fortnite using Project Reboot!
🚀 Developed by Auties00 - Version 10.0.7
""".green());
final parser = ConsoleParser(
commands: [
_build,
_play,
_host,
_backend
]
);
final command = parser.parse(args);
await _handleRootCommand(command);
}
Future<void> _handleRootCommand(CommandCall? command) async {
if(command == null) {
await _askRootCommand();
return;
}
switch (command.name) {
case 'versions':
await _handleBuildCommand(command.subCall);
break;
case 'play':
_handlePlayCommand(command.subCall);
break;
case 'host':
_handleHostCommand(command.subCall);
break;
case 'backend':
_handleBackendCommand(command.subCall);
break;
default:
await _askRootCommand();
break;
}
}
Future<void> _askRootCommand() async {
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: '', successSuffix: '', errorPrefix: '')
);
await _handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
}
Future<void> _handleBuildCommand(CommandCall? call) async {
if(call == null) {
_askBuildCommand();
return;
}
switch(call.name) {
case 'import':
await _handleBuildImportCommand(call);
break;
case 'download':
_handleBuildDownloadCommand(call);
break;
case 'list':
_handleBuildListCommand(call);
break;
default:
_askBuildCommand();
break;
}
}
void _handleBuildListCommand(CommandCall commandCall) {
List<FortniteVersion> versions;
try {
versions = readVersions();
}catch(error) {
print("$error");
return;
}
if(versions.isEmpty) {
print("❌ No versions found");
return;
}
final versionSelector = Select.withTheme(
prompt: ' Select a version:',
options: versions.map((version) => version.gameVersion).toList(growable: false),
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
final version = versions[versionSelector.interact()];
final actionSelector = Select.withTheme(
prompt: ' Select an action:',
options: _versionActions,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
final action = _versionActions[actionSelector.interact()];
switch(action) {
case _playVersionAction:
break;
case _hostVersionAction:
break;
case _deleteVersionAction:
break;
case _infoVersionAction:
print('');
print("""
🏷️ ${"Version: ".cyan()} ${version.gameVersion}
📁 ${"Location:".cyan()} ${version.location.path}
""".green());
break;
}
}
Future<void> _handleBuildImportCommand(CommandCall call) async {
final version = _getOrPromptVersion(call);
if(version == null) {
return;
}
final path = await _getOrPromptPath(call, true);
if(path == null) {
return;
}
final fortniteVersion = FortniteVersion(
name: '',
gameVersion: version,
location: Directory(path)
);
writeVersion(fortniteVersion);
print('');
print('✅ Imported build: ${version.green()}');
}
String? _getOrPromptVersion(CommandCall call) {
final version = call.parameters['version'];
if(version != null) {
final result = version.trim();
if (_versions.contains(result)) {
return result;
}
print('');
print("❌ Unknown version: $result");
return null;
}
stdout.write('❓ Type a version: ');
final result = runAutoComplete(_autocompleteVersion).trim();
if(_versions.contains(result)) {
print('✅ Type a version: ${result.green()}');
return result;
}
print('');
print("❌ Unknown version: $version");
return null;
}
Future<String?> _getOrPromptPath(CommandCall call, bool existing) async {
var path = call.parameters['path'];
if(path != null) {
final result = path.trim();
final check = await _checkBuildPath(result, existing);
if(!check) {
return null;
}
return result;
}
stdout.write('❓ Type a path: ');
final result = runAutoComplete(_autocompletePath).trim();
final check = await _checkBuildPath(result, existing);
if(!check) {
return null;
}
print('✅ Type a path: ${result.green()}');
return result;
}
Future<bool> _checkBuildPath(String path, bool existing) async {
final directory = Directory(path);
if(!directory.existsSync()) {
if(existing) {
print('');
print("❌ Unknown path: $path");
return false;
}else {
directory.createSync(recursive: true);
}
}
if (existing) {
final checker = Spinner.withTheme(
icon: '',
rightPrompt: (status) {
switch(status) {
case SpinnerStateType.inProgress:
return 'Looking for FortniteClient-Win64-Shipping.exe...';
case SpinnerStateType.done:
return 'Finished looking for FortniteClient-Win64-Shipping.exe';
case SpinnerStateType.failed:
return 'Failed to look for FortniteClient-Win64-Shipping.exe';
}
},
theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' '))
).interact();
final files = await findFiles(directory, "FortniteClient-Win64-Shipping.exe")
.withMinimumDuration(const Duration(seconds: 1));
if(files.isEmpty) {
print("❌ Cannot find FortniteClient-Win64-Shipping.exe in $path");
return false;
}
if(files.length > 1) {
print("❌ There must be only one executable named FortniteClient-Win64-Shipping.exe in $path");
return false;
}
checker.done();
}
return true;
}
String? _autocompleteVersion(String input) => input.isEmpty ? null : _versions.firstWhereOrNull((version) => version.toLowerCase().startsWith(input.toLowerCase()));
String? _autocompletePath(String path) {
try {
if (path.isEmpty) {
return null;
}
final String separator = Platform.isWindows ? '\\' : '/';
path = path.replaceAll('\\', separator);
if (FileSystemEntity.isFileSync(path)) {
return null;
}
if(FileSystemEntity.isDirectorySync(path)) {
return path.endsWith(separator) ? null : path + separator;
}
final lastSeparatorIndex = path.lastIndexOf(separator);
String directoryPath;
String partialName;
String prefixPath;
if (lastSeparatorIndex == -1) {
directoryPath = '';
partialName = path;
prefixPath = '';
} else {
directoryPath = path.substring(0, lastSeparatorIndex);
partialName = path.substring(lastSeparatorIndex + 1);
prefixPath = path.substring(0, lastSeparatorIndex + 1);
if (directoryPath.isEmpty && lastSeparatorIndex == 0) {
directoryPath = separator;
} else if (directoryPath.isEmpty) {
directoryPath = '.';
}
}
final dir = Directory(directoryPath);
if (!dir.existsSync()) {
return null;
}
final entries = dir.listSync();
final matches = <FileSystemEntity>[];
for (var entry in entries) {
final name = entry.path.split(separator).last;
if (name.startsWith(partialName)) {
matches.add(entry);
}
}
if (matches.isEmpty) {
return null;
}
matches.sort((a, b) {
final aIsDir = a is Directory;
final bIsDir = b is Directory;
if (aIsDir != bIsDir) {
return aIsDir ? -1 : 1;
}
final aName = a.path.split(separator).last;
final bName = b.path.split(separator).last;
if (aName.length != bName.length) {
return aName.length - bName.length;
}
return aName.compareTo(bName);
});
final bestMatch = matches.first;
final bestMatchName = bestMatch.path.split(separator).last;
var result = prefixPath + bestMatchName;
if (bestMatch is Directory) {
result = result.endsWith(separator) ? result : result + separator;
}
return result;
} catch (_) {
return null;
}
}
Future<void> _handleBuildDownloadCommand(CommandCall call) async {
final version = _getOrPromptVersion(call);
if(version == null) {
return;
}
final parsedVersion = Version.parse(version);
final build = downloadableBuilds.firstWhereOrNull((build) => Version.parse(build.gameVersion) == parsedVersion);
if(build == null) {
print('');
print("❌ Cannot find mirror for version: $parsedVersion");
return;
}
final path = await _getOrPromptPath(call, false);
if(path == null) {
return;
}
double progress = 0;
bool extracting = false;
final downloader = Spinner.withTheme(
icon: '',
rightPrompt: (status) => status != SpinnerStateType.inProgress ? 'Finished ${extracting ? 'extracting' : 'downloading'} ${parsedVersion.toString()}' : '${extracting ? 'Extracting' : 'Downloading'} ${parsedVersion.toString()} (${progress.round()}%)...',
theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' '))
).interact();
final parsedDirectory = Directory(path);
final receivePort = ReceivePort();
SendPort? sendPort;
receivePort.listen((message) {
if(message is FortniteBuildDownloadProgress) {
if(message.progress >= 100) {
sendPort?.send(kStopBuildDownloadSignal);
stopDownloadServer();
downloader.done();
receivePort.close();
final fortniteVersion = FortniteVersion(
name: "dummy",
gameVersion: version,
location: parsedDirectory
);
writeVersion(fortniteVersion);
print('');
print('✅ Downloaded build: ${version.green()}');
}else {
progress = message.progress;
extracting = message.extracting;
}
}else if(message is SendPort) {
sendPort = message;
}else {
sendPort?.send(kStopBuildDownloadSignal);
stopDownloadServer();
downloader.done();
receivePort.close();
print("❌ Cannot download build: $message");
}
});
final options = FortniteBuildDownloadOptions(
build,
parsedDirectory,
receivePort.sendPort
);
await downloadArchiveBuild(options);
}
void _askBuildCommand() {
final commands = [_buildList.name, _buildImport.name, _buildDownload.name];
final commandSelector = Select.withTheme(
prompt: ' Select a version command:',
options: commands,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
_handleBuildCommand(CommandCall(name: commands[commandSelector.interact()]));
}
void _handlePlayCommand(CommandCall? call) {
}
void _handleHostCommand(CommandCall? call) {
}
void _handleBackendCommand(CommandCall? call) {
}

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,34 @@
import 'dart:convert';
import 'dart:io';
import 'package:reboot_common/common.dart';
List<FortniteVersion> readVersions() {
final file = _versionsFile;
if(!file.existsSync()) {
return [];
}
try {
Iterable decodedVersionsJson = jsonDecode(file.readAsStringSync());
return decodedVersionsJson
.map((entry) {
try {
return FortniteVersion.fromJson(entry);
}catch(error) {
throw "Cannot parse version: $error";
}
})
.toList();
}catch(error) {
throw "Cannot parse versions: $error";
}
}
void writeVersion(FortniteVersion version) {
final versions = readVersions();
versions.add(version);
_versionsFile.writeAsString(jsonEncode(versions.map((version) => version.toJson()).toList()));
}
File get _versionsFile => File('${installationDirectory.path}/versions.json');

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

@@ -0,0 +1,160 @@
import 'dart:io';
import 'package:dart_console/dart_console.dart';
typedef AutoComplete = String? Function(String);
class ConsoleParser {
final List<Command> commands;
ConsoleParser({required this.commands});
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;
}
Set<String> _toParameters(Command? parent) => parent?.parameters
.map((e) => '--${e.toLowerCase()}')
.toSet() ?? {};
Map<String, Command> _toMap(List<Command> children) => Map.fromIterable(
children,
key: (command) => command.name.toLowerCase(),
value: (command) => command
);
}
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}';
}
String runAutoComplete(AutoComplete completion) {
final console = Console();
console.rawMode = true;
final position = console.cursorPosition!;
var currentInput = '';
var running = true;
var result = '';
while (running) {
final key = console.readKey();
switch (key.controlChar) {
case ControlCharacter.ctrlC:
running = false;
break;
case ControlCharacter.enter:
_eraseUntil(console, position);
console.write(currentInput);
console.writeLine();
result = currentInput;
running = false;
break;
case ControlCharacter.tab:
final suggestion = completion(currentInput);
if (suggestion != null) {
_eraseUntil(console, position);
currentInput = suggestion;
console.write(currentInput);
}
break;
case ControlCharacter.backspace:
if (currentInput.isNotEmpty) {
currentInput = currentInput.substring(0, currentInput.length - 1);
_eraseUntil(console, position);
console.write(currentInput);
_showSuggestion(console, position, currentInput, completion);
}
break;
default:
currentInput += key.char;
console.write(key.char);
_showSuggestion(console, position, currentInput, completion);
}
}
return result;
}
void _eraseUntil(Console console, Coordinate position) {
console.cursorPosition = position;
stdout.write('\x1b[K');
}
void _showSuggestion(Console console, Coordinate position, String input, AutoComplete completion) {
final suggestion = completion(input);
if(suggestion == null) {
_eraseUntil(console, position);
console.write(input);
}else if(suggestion.length > input.length) {
final remaining = suggestion.substring(input.length);
final cursorPosition = console.cursorPosition;
console.setForegroundColor(ConsoleColor.brightBlack);
console.write(remaining);
console.resetColorAttributes();
console.cursorPosition = cursorPosition;
}
}

View File

@@ -0,0 +1,20 @@
extension IterableExtension<E> on Iterable<E> {
E? firstWhereOrNull(bool test(E element)) {
for (final element in this) {
if (test(element)) {
return element;
}
}
return null;
}
}
extension FutureExtension<T> on Future<T> {
Future<T> withMinimumDuration(Duration duration) async {
final result = await Future.wait([
Future.delayed(duration),
this
]);
return result.last;
}
}

View File

@@ -1,17 +1,19 @@
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: ">=3.0.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_cli: ^2.4.0
args: ^2.6.0
version: ^3.0.2
dependency_overrides: dependency_overrides:
xml: ^6.3.0 xml: ^6.3.0

View File

@@ -1,8 +1,6 @@
export 'package:reboot_common/src/constant/backend.dart'; export 'package:reboot_common/src/constant/backend.dart';
export 'package:reboot_common/src/constant/game.dart'; export 'package:reboot_common/src/constant/game.dart';
export 'package:reboot_common/src/constant/supabase.dart'; export 'package:reboot_common/src/constant/supabase.dart';
export 'package:reboot_common/src/extension/path.dart';
export 'package:reboot_common/src/extension/process.dart';
export 'package:reboot_common/src/model/fortnite_build.dart'; export 'package:reboot_common/src/model/fortnite_build.dart';
export 'package:reboot_common/src/model/fortnite_version.dart'; export 'package:reboot_common/src/model/fortnite_version.dart';
export 'package:reboot_common/src/model/game_instance.dart'; export 'package:reboot_common/src/model/game_instance.dart';
@@ -13,10 +11,8 @@ export 'package:reboot_common/src/model/update_timer.dart';
export 'package:reboot_common/src/model/fortnite_server.dart'; export 'package:reboot_common/src/model/fortnite_server.dart';
export 'package:reboot_common/src/model/dll.dart'; export 'package:reboot_common/src/model/dll.dart';
export 'package:reboot_common/src/util/backend.dart'; export 'package:reboot_common/src/util/backend.dart';
export 'package:reboot_common/src/util/build.dart'; export 'package:reboot_common/src/util/downloader.dart';
export 'package:reboot_common/src/util/dll.dart'; export 'package:reboot_common/src/util/os.dart';
export 'package:reboot_common/src/util/network.dart';
export 'package:reboot_common/src/util/patcher.dart';
export 'package:reboot_common/src/util/path.dart';
export 'package:reboot_common/src/util/process.dart';
export 'package:reboot_common/src/util/log.dart'; export 'package:reboot_common/src/util/log.dart';
export 'package:reboot_common/src/util/game.dart';
export 'package:reboot_common/src/util/extensions.dart';

View File

@@ -1,3 +1,5 @@
import 'package:version/version.dart';
const String kDefaultPlayerName = "Player"; const String kDefaultPlayerName = "Player";
const String kDefaultHostName = "Host"; const String kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1"; const String kDefaultGameServerHost = "127.0.0.1";
@@ -21,5 +23,12 @@ const List<String> kCannotConnectErrors = [
"Network failure when attempting to check platform restrictions", "Network failure when attempting to check platform restrictions",
"UOnlineAccountCommon::ForceLogout" "UOnlineAccountCommon::ForceLogout"
]; ];
const String kGameFinishedLine = "PlayersLeft: 1"; const String kGameFinishedLine = "TeamsLeft: 1";
const String kDisplayInitializedLine = "Display"; const String kDisplayLine = "Display";
const String kDisplayInitializedLine = "Initialized";
const String kShippingExe = "FortniteClient-Win64-Shipping.exe";
const String kLauncherExe = "FortniteLauncher.exe";
const String kEacExe = "FortniteClient-Win64-Shipping_EAC.exe";
const String kCrashReportExe = "CrashReportClient.exe";
const String kGFSDKAftermathLibDll = "GFSDK_Aftermath_Lib.dll";
final Version kMaxAllowedVersion = Version.parse("30.10");

View File

@@ -1,54 +0,0 @@
import 'dart:io';
import 'dart:isolate';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
extension FortniteVersionExtension on FortniteVersion {
static String _marker = "FortniteClient.mod";
static File? findFile(Directory directory, String name) {
try{
for(final child in directory.listSync()) {
if(child is Directory) {
if(!path.basename(child.path).startsWith("\.")) {
final result = findFile(child, name);
if(result != null) {
return result;
}
}
}else if(child is File) {
if(path.basename(child.path) == name) {
return child;
}
}
}
return null;
}catch(_){
return null;
}
}
Future<File?> get shippingExecutable async {
final result = findFile(location, "FortniteClient-Win64-Shipping.exe");
if(result == null) {
return null;
}
final marker = findFile(location, _marker);
if(marker != null) {
return result;
}
await Isolate.run(() => patchHeadless(result));
await File("${location.path}\\$_marker").create();
return result;
}
File? get launcherExecutable => findFile(location, "FortniteLauncher.exe");
File? get eacExecutable => findFile(location, "FortniteClient-Win64-Shipping_EAC.exe");
File? get splashBitmap => findFile(location, "Splash.bmp");
}

View File

@@ -1,15 +0,0 @@
extension StringExtension on String {
bool get isBlank {
if(isEmpty) {
return true;
}
for(var char in this.split("")) {
if(char != " ") {
return false;
}
}
return true;
}
}

View File

@@ -1,15 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:version/version.dart';
class FortniteBuild { class FortniteBuild {
final Version version; final String gameVersion;
final String link; final String link;
final bool available; final bool available;
FortniteBuild({ FortniteBuild({
required this.version, required this.gameVersion,
required this.link, required this.link,
required this.available required this.available
}); });

View File

@@ -1,22 +1,20 @@
import 'dart:io'; import 'dart:io';
import 'package:version/version.dart';
class FortniteVersion { class FortniteVersion {
Version content; String name;
String gameVersion;
Directory location; Directory location;
FortniteVersion.fromJson(json) FortniteVersion.fromJson(json)
: content = Version.parse(json["content"]), : name = json["name"],
gameVersion = json["gameVersion"],
location = Directory(json["location"]); location = Directory(json["location"]);
FortniteVersion({required this.content, required this.location}); FortniteVersion({required this.name, required this.gameVersion, required this.location});
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'content': content.toString(), 'name': name,
'gameVersion': gameVersion,
'location': location.path 'location': location.path
}; };
@override
bool operator ==(Object other) => other is FortniteVersion && this.content == other.content;
} }

View File

@@ -1,30 +1,30 @@
import 'dart:io'; import 'dart:io';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:version/version.dart';
class GameInstance { class GameInstance {
final Version version; final String version;
final bool host;
final int gamePid; final int gamePid;
final int? launcherPid; final int? launcherPid;
final int? eacPid; final int? eacPid;
final List<InjectableDll> injectedDlls; final List<InjectableDll> injectedDlls;
final GameServerType? serverType; final bool headless;
bool launched; bool launched;
bool movedToVirtualDesktop;
bool tokenError; bool tokenError;
bool killed; bool killed;
GameInstance? child; GameInstance? child;
GameInstance({ GameInstance({
required this.version, required this.version,
required this.host,
required this.gamePid, required this.gamePid,
required this.launcherPid, required this.launcherPid,
required this.eacPid, required this.eacPid,
required this.serverType, required this.headless,
required this.child required this.child
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = []; }): tokenError = false, killed = false, launched = false, injectedDlls = [];
void kill() { void kill() {
GameInstance? child = this; GameInstance? child = this;
@@ -35,20 +35,16 @@ class GameInstance {
} }
void _kill() { void _kill() {
if(!killed) {
launched = true; launched = true;
killed = true; killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt); Process.killPid(gamePid, ProcessSignal.sigabrt);
if(launcherPid != null) { if (launcherPid != null) {
Process.killPid(launcherPid!, ProcessSignal.sigabrt); Process.killPid(launcherPid!, ProcessSignal.sigabrt);
} }
if(eacPid != null) { if (eacPid != null) {
Process.killPid(eacPid!, ProcessSignal.sigabrt); Process.killPid(eacPid!, ProcessSignal.sigabrt);
} }
} }
} }
enum GameServerType {
headless,
virtualWindow,
window
} }

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

@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:ini/ini.dart'; import 'package:ini/ini.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart';
import 'package:shelf/shelf_io.dart'; import 'package:shelf/shelf_io.dart';
import 'package:shelf_proxy/shelf_proxy.dart'; import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:sync/semaphore.dart'; import 'package:sync/semaphore.dart';
@@ -15,17 +15,152 @@ 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,
window: detached, window: detached,
); );
process.stdOutput.listen((message) => log("[BACKEND] Message: $message")); process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
var killed = false;
process.stdError.listen((error) { process.stdError.listen((error) {
if(!killed) {
log("[BACKEND] Error: $error"); log("[BACKEND] Error: $error");
killed = true;
process.kill(ProcessSignal.sigterm);
onError?.call(error); onError?.call(error);
}
}); });
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); if(!detached) {
process.exitCode.then((exitCode) {
if(!killed) {
log("[BACKEND] Exit code: $exitCode");
onError?.call("Exit code: $exitCode");
killed = true;
}
});
}
return process; return process;
} }
@@ -111,7 +246,7 @@ Future<void> writeMatchmakingIp(String text) async {
final splitIndex = text.indexOf(":"); final splitIndex = text.indexOf(":");
final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text; final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort; var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
if(port.isBlank) { if(port.isBlankOrEmpty) {
port = kDefaultGameServerPort; port = kDefaultGameServerPort;
} }

View File

@@ -1,401 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart';
import 'package:uuid/uuid.dart';
import 'package:version/version.dart';
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*)?))%\$");
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);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
try {
final stopped = _setupLifecycle(options);
await outputFile.parent.create(recursive: true);
final downloadItemCompleter = Completer<File>();
await _startAriaServer();
final downloadId = await _startAriaDownload(options, outputFile);
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.tellStatus",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
}
final result = statusResponseJson["result"];
final files = result["files"] as List?;
if(files == null || files.isEmpty) {
downloadItemCompleter.completeError("Download aborted");
timer.cancel();
return;
}
final error = result["errorCode"];
if(error != null) {
final errorCode = int.tryParse(error);
if(errorCode == 0) {
final path = File(files[0]["path"]);
downloadItemCompleter.complete(path);
}else if(errorCode == 3) {
downloadItemCompleter.completeError("This build is not available yet");
}else {
final errorMessage = result["errorMessage"];
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
}
timer.cancel();
return;
}
final speed = int.parse(result["downloadSpeed"] ?? "0");
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
final totalLength = int.parse(files[0]["length"] ?? "0");
final percentage = completedLength * 100 / totalLength;
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
_onProgress(
options.port,
percentage,
speed,
minutesLeft,
false
);
}catch(error) {
throw "Invalid download status (${error})";
}
});
await Future.any([stopped.future, downloadItemCompleter.future]);
if(!stopped.isCompleted) {
final extension = path.extension(fileName);
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
}else {
await _stopAriaDownload(downloadId);
}
}catch(error) {
_onError(error, options);
}finally {
delete(outputFile);
}
}
Future<void> _startAriaServer() async {
await stopDownloadServer();
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe";
}
final process = await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-secret=$_ariaSecret",
"--rpc-listen-port=$_ariaPort",
"--file-allocation=none"
],
window: false
);
process.stdOutput.listen((message) => log("[ARIA] Message: $message"));
process.stdError.listen((error) => log("[ARIA] Error: $error"));
process.exitCode.then((exitCode) => log("[ARIA] Exit code: $exitCode"));
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) {
return;
}
await Future.delayed(const Duration(seconds: 1));
}
throw "cannot start download server (timeout exceeded)";
}
Future<bool> _isAriaRunning() async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.getVersion",
"params": [
"token:${_ariaSecret}"
]
};
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return response.statusCode == 200;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
"token:${_ariaSecret}",
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> stopDownloadServer() async {
await killProcessByPort(_ariaPort);
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
throw "Missing 7zip.exe";
}
process = await startProcess(
executable: sevenZip,
args: [
"x",
"-bsp1",
'-o"${options.destination.path}"',
"-y",
'"${tempFile.path}"'
],
);
var completed = false;
process.stdOutput.listen((data) {
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = data.trim().split(" ")[0];
if(!element.endsWith("%")) {
return;
}
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted zip archive", options);
}
});
break;
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
throw "Missing winrar.exe";
}
process = await startProcess(
executable: winrar,
args: [
"x",
"-o+",
'"${tempFile.path}"',
"*.*",
'"${options.destination.path}"'
]
);
var completed = false;
process.stdOutput.listen((data) {
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) {
return;
}
final percentage = int.parse(element).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted rar archive", options);
}
});
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
}
await Future.any([stopped.future, process.exitCode]);
process.kill(ProcessSignal.sigabrt);
}
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
if(percentage == 0) {
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: null,
speed: speed
));
return;
}
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: minutesLeft,
speed: speed
));
}
void _onError(Object? error, FortniteBuildDownloadOptions options) {
if(error != null) {
options.port.send(error.toString());
}
}
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
lifecyclePort.listen((message) {
if(message == kStopBuildDownloadSignal && !stopped.isCompleted) {
stopped.complete();
}
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}

View File

@@ -1,73 +0,0 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
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";
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();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
String? name;
switch(dll) {
case InjectableDll.console:
name = "console.dll";
case InjectableDll.auth:
name = "starfall.dll";
case InjectableDll.memoryLeak:
name = "memory.dll";
case InjectableDll.gameServer:
name = null;
}
if(name == null) {
return;
}
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}
final output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes, flush: true);
}
Future<void> downloadRebootDll(File file, String url) async {
Directory? outputDir;
try {
final response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
throw Exception("Cannot download reboot.zip: status code ${response.statusCode}");
}
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

View File

@@ -0,0 +1,560 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:uuid/uuid.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";
const String kStopBuildDownloadSignal = "kill";
final int _ariaPort = 6800;
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
final List<FortniteBuild> downloadableBuilds = [
FortniteBuild(gameVersion: "1.7.2", link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
FortniteBuild(gameVersion: "1.8", link: "https://public.simplyblk.xyz/1.8.rar", available: true),
FortniteBuild(gameVersion: "1.8.1", link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
FortniteBuild(gameVersion: "1.8.2", link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
FortniteBuild(gameVersion: "1.9", link: "https://public.simplyblk.xyz/1.9.rar", available: true),
FortniteBuild(gameVersion: "1.9.1", link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
FortniteBuild(gameVersion: "1.10", link: "https://public.simplyblk.xyz/1.10.rar", available: true),
FortniteBuild(gameVersion: "1.11", link: "https://public.simplyblk.xyz/1.11.zip", available: true),
FortniteBuild(gameVersion: "2.1.0", link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
FortniteBuild(gameVersion: "2.2.0", link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
FortniteBuild(gameVersion: "2.3", link: "https://public.simplyblk.xyz/2.3.rar", available: true),
FortniteBuild(gameVersion: "2.4.0", link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
FortniteBuild(gameVersion: "2.4.2", link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
FortniteBuild(gameVersion: "2.5.0", link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
FortniteBuild(gameVersion: "3.0", link: "https://public.simplyblk.xyz/3.0.zip", available: true),
FortniteBuild(gameVersion: "3.1", link: "https://public.simplyblk.xyz/3.1.rar", available: true),
FortniteBuild(gameVersion: "3.1.1", link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
FortniteBuild(gameVersion: "3.2", link: "https://public.simplyblk.xyz/3.2.zip", available: true),
FortniteBuild(gameVersion: "3.3", link: "https://public.simplyblk.xyz/3.3.rar", available: true),
FortniteBuild(gameVersion: "3.5", link: "https://public.simplyblk.xyz/3.5.rar", available: true),
FortniteBuild(gameVersion: "3.6", link: "https://public.simplyblk.xyz/3.6.zip", available: true),
FortniteBuild(gameVersion: "4.0", link: "https://public.simplyblk.xyz/4.0.zip", available: true),
FortniteBuild(gameVersion: "4.1", link: "https://public.simplyblk.xyz/4.1.zip", available: true),
FortniteBuild(gameVersion: "4.2", link: "https://public.simplyblk.xyz/4.2.zip", available: true),
FortniteBuild(gameVersion: "4.4", link: "https://public.simplyblk.xyz/4.4.rar", available: true),
FortniteBuild(gameVersion: "4.5", link: "https://public.simplyblk.xyz/4.5.rar", available: true),
FortniteBuild(gameVersion: "5.00", link: "https://public.simplyblk.xyz/5.00.rar", available: true),
FortniteBuild(gameVersion: "5.0.1", link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
FortniteBuild(gameVersion: "5.10", link: "https://public.simplyblk.xyz/5.10.rar", available: true),
FortniteBuild(gameVersion: "5.21", link: "https://public.simplyblk.xyz/5.21.rar", available: true),
FortniteBuild(gameVersion: "5.30", link: "https://public.simplyblk.xyz/5.30.rar", available: true),
FortniteBuild(gameVersion: "5.40", link: "https://public.simplyblk.xyz/5.40.rar", available: true),
FortniteBuild(gameVersion: "6.00", link: "https://public.simplyblk.xyz/6.00.rar", available: true),
FortniteBuild(gameVersion: "6.01", link: "https://public.simplyblk.xyz/6.01.rar", available: true),
FortniteBuild(gameVersion: "6.1.1", link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
FortniteBuild(gameVersion: "6.02", link: "https://public.simplyblk.xyz/6.02.rar", available: true),
FortniteBuild(gameVersion: "6.2.1", link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
FortniteBuild(gameVersion: "6.10", link: "https://public.simplyblk.xyz/6.10.rar", available: true),
FortniteBuild(gameVersion: "6.10.1", link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
FortniteBuild(gameVersion: "6.10.2", link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
FortniteBuild(gameVersion: "6.21", link: "https://public.simplyblk.xyz/6.21.rar", available: true),
FortniteBuild(gameVersion: "6.22", link: "https://public.simplyblk.xyz/6.22.rar", available: true),
FortniteBuild(gameVersion: "6.30", link: "https://public.simplyblk.xyz/6.30.rar", available: true),
FortniteBuild(gameVersion: "6.31", link: "https://public.simplyblk.xyz/6.31.rar", available: true),
FortniteBuild(gameVersion: "7.00", link: "https://public.simplyblk.xyz/7.00.rar", available: true),
FortniteBuild(gameVersion: "7.10", link: "https://public.simplyblk.xyz/7.10.rar", available: true),
FortniteBuild(gameVersion: "7.20", link: "https://public.simplyblk.xyz/7.20.rar", available: true),
FortniteBuild(gameVersion: "7.30", link: "https://public.simplyblk.xyz/7.30.zip", available: true),
FortniteBuild(gameVersion: "7.40", link: "https://public.simplyblk.xyz/7.40.rar", available: true),
FortniteBuild(gameVersion: "8.00", link: "https://public.simplyblk.xyz/8.00.zip", available: true),
FortniteBuild(gameVersion: "8.20", link: "https://public.simplyblk.xyz/8.20.rar", available: true),
FortniteBuild(gameVersion: "8.30", link: "https://public.simplyblk.xyz/8.30.rar", available: true),
FortniteBuild(gameVersion: "8.40", link: "https://public.simplyblk.xyz/8.40.zip", available: true),
FortniteBuild(gameVersion: "8.50", link: "https://public.simplyblk.xyz/8.50.zip", available: true),
FortniteBuild(gameVersion: "8.51", link: "https://public.simplyblk.xyz/8.51.rar", available: true),
FortniteBuild(gameVersion: "9.00", link: "https://public.simplyblk.xyz/9.00.zip", available: true),
FortniteBuild(gameVersion: "9.01", link: "https://public.simplyblk.xyz/9.01.zip", available: true),
FortniteBuild(gameVersion: "9.10", link: "https://public.simplyblk.xyz/9.10.rar", available: true),
FortniteBuild(gameVersion: "9.21", link: "https://public.simplyblk.xyz/9.21.zip", available: true),
FortniteBuild(gameVersion: "9.30", link: "https://public.simplyblk.xyz/9.30.zip", available: true),
FortniteBuild(gameVersion: "9.40", link: "https://public.simplyblk.xyz/9.40.zip", available: true),
FortniteBuild(gameVersion: "9.41", link: "https://public.simplyblk.xyz/9.41.rar", available: true),
FortniteBuild(gameVersion: "10.00", link: "https://public.simplyblk.xyz/10.00.zip", available: true),
FortniteBuild(gameVersion: "10.10", link: "https://public.simplyblk.xyz/10.10.zip", available: true),
FortniteBuild(gameVersion: "10.20", link: "https://public.simplyblk.xyz/10.20.zip", available: true),
FortniteBuild(gameVersion: "10.31", link: "https://public.simplyblk.xyz/10.31.zip", available: true),
FortniteBuild(gameVersion: "10.40", link: "https://public.simplyblk.xyz/10.40.rar", available: true),
FortniteBuild(gameVersion: "11.00", link: "https://public.simplyblk.xyz/11.00.zip", available: true),
FortniteBuild(gameVersion: "11.31", link: "https://public.simplyblk.xyz/11.31.rar", available: true),
FortniteBuild(gameVersion: "12.00", link: "https://public.simplyblk.xyz/12.00.rar", available: true),
FortniteBuild(gameVersion: "12.21", link: "https://public.simplyblk.xyz/12.21.zip", available: true),
FortniteBuild(gameVersion: "12.50", link: "https://public.simplyblk.xyz/12.50.zip", available: true),
FortniteBuild(gameVersion: "12.61", link: "https://public.simplyblk.xyz/12.61.zip", available: true),
FortniteBuild(gameVersion: "13.00", link: "https://public.simplyblk.xyz/13.00.rar", available: true),
FortniteBuild(gameVersion: "13.40", link: "https://public.simplyblk.xyz/13.40.zip", available: true),
FortniteBuild(gameVersion: "14.00", link: "https://public.simplyblk.xyz/14.00.rar", available: true),
FortniteBuild(gameVersion: "14.40", link: "https://public.simplyblk.xyz/14.40.rar", available: true),
FortniteBuild(gameVersion: "14.60", link: "https://public.simplyblk.xyz/14.60.rar", available: true),
FortniteBuild(gameVersion: "15.30", link: "https://public.simplyblk.xyz/15.30.rar", available: true),
FortniteBuild(gameVersion: "16.40", link: "https://public.simplyblk.xyz/16.40.rar", available: true),
FortniteBuild(gameVersion: "17.30", link: "https://public.simplyblk.xyz/17.30.zip", available: true),
FortniteBuild(gameVersion: "17.50", link: "https://public.simplyblk.xyz/17.50.zip", available: true),
FortniteBuild(gameVersion: "18.40", link: "https://public.simplyblk.xyz/18.40.zip", available: true),
FortniteBuild(gameVersion: "19.10", link: "https://public.simplyblk.xyz/19.10.rar", available: true),
FortniteBuild(gameVersion: "20.40", link: "https://public.simplyblk.xyz/20.40.zip", available: true),
];
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
Timer? timer;
try {
final stopped = _setupLifecycle(options);
await outputFile.parent.create(recursive: true);
final downloadItemCompleter = Completer<File>();
await _startAriaServer();
final downloadId = await _startAriaDownload(options, outputFile);
timer = Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.tellStatus",
"params": [
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
}
final result = statusResponseJson["result"];
final files = result["files"] as List?;
if(files == null || files.isEmpty) {
downloadItemCompleter.completeError("Download aborted");
timer.cancel();
return;
}
final error = result["errorCode"];
if(error != null) {
final errorCode = int.tryParse(error);
if(errorCode == 0) {
final path = File(files[0]["path"]);
downloadItemCompleter.complete(path);
}else if(errorCode == 3) {
downloadItemCompleter.completeError("This build is not available yet");
}else {
final errorMessage = result["errorMessage"];
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
}
timer.cancel();
return;
}
final speed = int.parse(result["downloadSpeed"] ?? "0");
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
final totalLength = int.parse(files[0]["length"] ?? "0");
final percentage = completedLength * 100 / totalLength;
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
_onProgress(
options.port,
percentage,
speed,
minutesLeft,
false
);
}catch(error) {
throw "Invalid download status (${error})";
}
});
await Future.any([stopped.future, downloadItemCompleter.future]);
if(!stopped.isCompleted) {
final extension = path.extension(fileName);
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
}else {
await _stopAriaDownload(downloadId);
}
}catch(error) {
_onError(error, options);
}finally {
delete(outputFile);
timer?.cancel();
}
}
Future<void> _startAriaServer() async {
await stopDownloadServer();
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe at ${aria2c.path}";
}
final process = await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-listen-port=$_ariaPort",
"--file-allocation=none",
"--check-certificate=false"
],
window: false
);
process.stdOutput.listen((message) => log("[ARIA] Message: $message"));
process.stdError.listen((error) => log("[ARIA] Error: $error"));
process.exitCode.then((exitCode) => log("[ARIA] Exit code: $exitCode"));
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) {
return;
}
await Future.delayed(const Duration(seconds: 1));
}
throw "cannot start download server (timeout exceeded)";
}
Future<bool> _isAriaRunning() async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.getVersion",
"params": [
]
};
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return response.statusCode == 200;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> stopDownloadServer() async {
await killProcessByPort(_ariaPort);
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
throw "Missing 7zip.exe";
}
process = await startProcess(
executable: sevenZip,
args: [
"x",
"-bsp1",
'-o"${options.destination.path}"',
"-y",
'"${tempFile.path}"'
],
);
var completed = false;
process.stdOutput.listen((data) {
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = data.trim().split(" ")[0];
if(!element.endsWith("%")) {
return;
}
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlankOrEmpty) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted zip archive", options);
}
});
break;
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
throw "Missing winrar.exe";
}
process = await startProcess(
executable: winrar,
args: [
"x",
"-o+",
'"${tempFile.path}"',
"*.*",
'"${options.destination.path}"'
]
);
var completed = false;
process.stdOutput.listen((data) {
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) {
return;
}
final percentage = int.parse(element).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlankOrEmpty) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted rar archive", options);
}
});
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
}
await Future.any([stopped.future, process.exitCode]);
process.kill(ProcessSignal.sigabrt);
}
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
if(percentage == 0) {
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: null,
speed: speed
));
return;
}
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: minutesLeft,
speed: speed
));
}
void _onError(Object? error, FortniteBuildDownloadOptions options) {
if(error != null) {
options.port.send(error.toString());
}
}
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
lifecyclePort.listen((message) {
if(message == kStopBuildDownloadSignal && !stopped.isCompleted) {
lifecyclePort.close();
stopped.complete();
}
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}
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();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
Future<bool> downloadDependency(InjectableDll dll, String outputPath) async {
String? name;
switch(dll) {
case InjectableDll.console:
name = "console.dll";
case InjectableDll.auth:
name = "cobalt.dll";
case InjectableDll.memoryLeak:
name = "memory.dll";
case InjectableDll.gameServer:
name = null;
}
if(name == null) {
return false;
}
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}
final output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes, flush: true);
try {
await output.readAsBytes();
return true;
}catch(_) {
return false;
}
}
Future<bool> downloadRebootDll(File file, String url, bool aboveS20) async {
Directory? outputDir;
try {
var response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
response = await http.get(Uri.parse(aboveS20 ? _kRebootAboveS20FallbackDownloadUrl : _kRebootBelowS20FallbackDownloadUrl));
if(response.statusCode != 200) {
throw "status code ${response.statusCode}";
}
}
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
try {
await tempZip.writeAsBytes(response.bodyBytes, flush: true); // Write reboot.zip to disk
await tempZip.readAsBytes(); // Check implicitly if antivirus doesn't like reboot
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = outputDir.listSync()
.firstWhere((element) => path.extension(element.path) == ".dll") as File;
final rebootDllSource = await rebootDll.readAsBytes();
await file.writeAsBytes(rebootDllSource, flush: true);
await file.readAsBytes(); // Check implicitly if antivirus doesn't like reboot
return true;
} catch(_) {
return false; // Anti virus probably flagged reboot
}
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

View File

@@ -6,3 +6,19 @@ extension ProcessExtension on Process {
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event, allowMalformed: true).split("\n")); Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event, allowMalformed: true).split("\n"));
} }
extension StringExtension on String {
bool get isBlankOrEmpty {
if(isEmpty) {
return true;
}
for(var char in this.split("")) {
if(char != " ") {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,383 @@
import 'dart:collection';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:reboot_common/common.dart';
import 'package:win32/win32.dart';
import 'package:path/path.dart' as path;
final DynamicLibrary _shell32 = DynamicLibrary.open('shell32.dll');
final SHGetPropertyStoreFromParsingName =
_shell32.lookupFunction<
Int32 Function(Pointer<Utf16> pszPath, Pointer<Void> pbc, Uint32 flags,
Pointer<GUID> riid, Pointer<Pointer<COMObject>> ppv),
int Function(Pointer<Utf16> pszPath, Pointer<Void> pbc, int flags,
Pointer<GUID> riid, Pointer<Pointer<COMObject>> ppv)>('SHGetPropertyStoreFromParsingName');
final Uint8List _originalHeadless = Uint8List.fromList([
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
]);
final Uint8List _patchedHeadless = Uint8List.fromList([
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
]);
// Not used right now
final Uint8List _originalMatchmaking = Uint8List.fromList([
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
]);
final Uint8List _patchedMatchmaking = Uint8List.fromList([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
// https://github.com/polynite/fn-releases
const Map<int, String> _buildToGameVersion = {
2870186: "1.0.0",
3700114: "1.7.2",
3724489: "1.8.0",
3729133: "1.8.1",
3741772: "1.8.2",
3757339: "1.9",
3775276: "1.9.1",
3790078: "1.10",
3807424: "1.11",
3825894: "2.1",
3841827: "2.2",
3847564: "2.3",
3858292: "2.4",
3870737: "2.4.2",
3889387: "2.5",
3901517: "3.0.0",
3915963: "3.1",
3917250: "3.1.1",
3935073: "3.2",
3942182: "3.3",
4008490: "3.5",
4019403: "3.6",
4039451: "4.0",
4053532: "4.1",
4072250: "4.2",
4117433: "4.4",
4127312: "4.4.1",
4159770: "4.5",
4204761: "5.0",
4214610: "5.01",
4240749: "5.10",
4288479: "5.21",
4305896: "5.30",
4352937: "5.40",
4363240: "5.41",
4395664: "6.0",
4424678: "6.01",
4461277: "6.0.2",
4464155: "6.10",
4476098: "6.10.1",
4480234: "6.10.2",
4526925: "6.21",
4543176: "6.22",
4573279: "6.31",
4629139: "7.0",
4667333: "7.10",
4727874: "7.20",
4834550: "7.30",
5046157: "7.40",
5203069: "8.00",
5625478: "8.20",
5793395: "8.30",
6005771: "8.40",
6058028: "8.50",
6165369: "8.51",
6337466: "9.00",
6428087: "9.01",
6639283: "9.10",
6922310: "9.21",
7095426: "9.30",
7315705: "9.40",
7609292: "9.41",
7704164: "10.00",
7955722: "10.10",
8456527: "10.20",
8723043: "10.31",
9380822: "10.40",
9603448: "11.00",
9901083: "11.10",
10708866: "11.30",
10800459: "11.31",
11265652: "11.50",
11556442: "12.00",
11883027: "12.10",
12353830: "12.21",
12905909: "12.41",
13137020: "12.50",
13498980: "12.61",
14113327: "13.40",
14211474: "14.00",
14456520: "14.30",
14550713: "14.40",
14786821: "14.60",
14835335: "15.00",
15014719: "15.10",
15341163: "15.30",
15526472: "15.50",
15913292: "16.10",
16163563: "16.30",
16218553: "16.40",
16469788: "16.50",
16745144: "17.10",
17004569: "17.30",
17269705: "17.40",
17388565: "17.50",
17468642: "18.00",
17661844: "18.10",
17745267: "18.20",
17811397: "18.21",
17882303: "18.30",
18163738: "18.40",
18489740: "19.01",
18675304: "19.10",
19458861: "20.00",
19598943: "20.10",
19751212: "20.20",
19950687: "20.30",
20244966: "20.40",
20463113: "21.00",
20696680: "21.10",
21035704: "21.20",
21657658: "21.50",
};
Future<bool> patchHeadless(File file) async =>
await _patch(file, _originalHeadless, _patchedHeadless);
Future<bool> patchMatchmaking(File file) async =>
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async => Isolate.run(() async {
try {
if(original.length != patched.length){
throw Exception("Cannot mutate length of binary file");
}
final source = await file.readAsBytes();
var readOffset = 0;
var patchOffset = -1;
var patchCount = 0;
while(readOffset < source.length){
if(source[readOffset] == original[patchCount]){
if(patchOffset == -1) {
patchOffset = readOffset;
}
if(readOffset - patchOffset + 1 == original.length) {
break;
}
patchCount++;
}else {
patchOffset = -1;
patchCount = 0;
}
readOffset++;
}
if(patchOffset == -1) {
return false;
}
for(var i = 0; i < patched.length; i++) {
source[patchOffset + i] = patched[i];
}
await file.writeAsBytes(source, flush: true);
return true;
}catch(_){
return false;
}
});
List<String> createRebootArgs(String username, String password, bool host, bool headless, bool logging, String additionalArgs) {
log("[PROCESS] Generating reboot args");
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
final args = LinkedHashMap<String, String>(
equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
hashCode: (a) => a.toUpperCase().hashCode
);
args.addAll({
"-epicapp": "Fortnite",
"-epicenv": "Prod",
"-epiclocale": "en-us",
"-epicportal": "",
"-skippatchcheck": "",
"-nobe": "",
"-fromfl": "eac",
"-fltoken": "3db3ba5dcbd2e16703f3978d",
"-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN": username,
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
"-AUTH_TYPE": "epic"
});
if(logging) {
args["-log"] = "";
}
if(host) {
args["-nosplash"] = "";
args["-nosound"] = "";
if(headless){
args["-nullrhi"] = "";
}
}
log("[PROCESS] Default args: $args");
log("[PROCESS] Adding custom args: $additionalArgs");
for(final additionalArg in additionalArgs.split(" ")) {
log("[PROCESS] Processing custom arg: $additionalArg");
final separatorIndex = additionalArg.indexOf("=");
final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex);
log("[PROCESS] Custom arg key: $argName");
final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1);
log("[PROCESS] Custom arg value: $argValue");
args[argName] = argValue;
log("[PROCESS] Updated args: $args");
}
log("[PROCESS] Final args result: $args");
return args.entries
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
.toList();
}
void handleGameOutput({
required String line,
required bool host,
required void Function() onLoggedIn,
required void Function() onMatchEnd,
required void Function() onShutdown,
required void Function() onTokenError,
required void Function() onBuildCorrupted,
}) {
if (line.contains(kShutdownLine)) {
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd();
}
}
String _parseUsername(String username, bool host) {
if (username.isEmpty) {
return kDefaultPlayerName;
}
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}
// Parsing the version is not that easy
// Also on some versions the shipping exe has it as well, but not on all: that's why i'm using the crash report client
// ++Fortnite+Release-34.10-CL-40567068
// 4.16.0-3700114+++Fortnite+Release-Cert
// 4.19.0-3870737+++Fortnite+Release-Next
// 4.20.0-4008490+++Fortnite+Release-3.5
Future<String> extractGameVersion(Directory directory) => Isolate.run(() async {
log("[VERSION] Looking for $kCrashReportExe in ${directory.path}");
final defaultGameVersion = path.basename(directory.path);
final crashReportClients = await findFiles(directory, kCrashReportExe);
if (crashReportClients.isEmpty) {
log("[VERSION] Didn't find a unique match: $crashReportClients");
return defaultGameVersion;
}
log("[VERSION] Extracting game version from ${crashReportClients.last.path}(default: $defaultGameVersion)");
final filePathPtr = crashReportClients.last.path.toNativeUtf16();
final pPropertyStore = calloc<COMObject>();
final iidPropertyStore = GUIDFromString(IID_IPropertyStore);
final ret = SHGetPropertyStoreFromParsingName(
filePathPtr,
nullptr,
GETPROPERTYSTOREFLAGS.GPS_DEFAULT,
iidPropertyStore,
pPropertyStore.cast()
);
calloc.free(filePathPtr);
calloc.free(iidPropertyStore);
if (FAILED(ret)) {
log("[VERSION] Using default value");
calloc.free(pPropertyStore);
return defaultGameVersion;
}
final propertyStore = IPropertyStore(pPropertyStore);
final countPtr = calloc<Uint32>();
final hrCount = propertyStore.getCount(countPtr);
final count = countPtr.value;
calloc.free(countPtr);
if (FAILED(hrCount)) {
log("[VERSION] Using default value");
return defaultGameVersion;
}
for (var i = 0; i < count; i++) {
final pKey = calloc<PROPERTYKEY>();
final hrKey = propertyStore.getAt(i, pKey);
if (FAILED(hrKey)) {
calloc.free(pKey);
continue;
}
final pv = calloc<PROPVARIANT>();
final hrValue = propertyStore.getValue(pKey, pv);
if (!FAILED(hrValue)) {
if (pv.ref.vt == VARENUM.VT_LPWSTR) {
final valueStr = pv.ref.pwszVal.toDartString();
final headerIndex = valueStr.indexOf("++Fortnite");
if (headerIndex != -1) {
log("[VERSION] Found value string: $valueStr");
var gameVersion = valueStr.substring(valueStr.indexOf("-", headerIndex) + 1);
log("[VERSION] Game version: $gameVersion");
if(gameVersion == "Cert" || gameVersion == "Next") {
final engineVersion = valueStr.substring(0, valueStr.indexOf("+"));
log("[VERSION] Engine version: $engineVersion");
final engineVersionParts = engineVersion.split("-");
final engineVersionBuild = int.parse(engineVersionParts[1]);
log("[VERSION] Engine build: $engineVersionBuild");
gameVersion = _buildToGameVersion[engineVersionBuild] ?? defaultGameVersion;
}
log("[VERSION] Parsed game version: $gameVersion");
return gameVersion;
}
}
}
calloc.free(pKey);
calloc.free(pv);
}
log("[VERSION] Using default value");
return defaultGameVersion;
});

View File

@@ -1,10 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart'; import 'package:synchronized/extension.dart';
final File launcherLogFile = _createLoggingFile(); final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1); bool enableLoggingToConsole = true;
File _createLoggingFile() { File _createLoggingFile() {
final file = File("${installationDirectory.path}\\launcher.log"); final file = File("${installationDirectory.path}\\launcher.log");
@@ -18,12 +18,14 @@ File _createLoggingFile() {
void log(String message) async { void log(String message) async {
try { try {
await _semaphore.acquire(); if(enableLoggingToConsole) {
print(message); print(message);
}
launcherLogFile.synchronized(() async {
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true); await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
});
}catch(error) { }catch(error) {
print("[LOGGER_ERROR] An error occurred while logging: $error"); print("[LOGGER_ERROR] An error occurred while logging: $error");
}finally {
_semaphore.release();
} }
} }

View File

@@ -1,96 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
const _AF_INET = 2;
const _TCP_TABLE_OWNER_PID_LISTENER = 3;
final _getExtendedTcpTable = DynamicLibrary.open('iphlpapi.dll').lookupFunction<
Int32 Function(Pointer, Pointer<Uint32>, Int32, Int32, Int32, Int32),
int Function(Pointer, Pointer<Uint32>, int, int, int, int)>('GetExtendedTcpTable');
final class _MIB_TCPROW_OWNER_PID extends Struct {
@Uint32()
external int dwState;
@Uint32()
external int dwLocalAddr;
@Uint32()
external int dwLocalPort;
@Uint32()
external int dwRemoteAddr;
@Uint32()
external int dwRemotePort;
@Uint32()
external int dwOwningPid;
}
final class _MIB_TCPTABLE_OWNER_PID extends Struct {
@Uint32()
external int dwNumEntries;
@Array(512)
external Array<_MIB_TCPROW_OWNER_PID> table;
}
bool isLocalHost(String host) => host.trim() == "127.0.0.1"
|| host.trim().toLowerCase() == "localhost"
|| host.trim() == "0.0.0.0";
bool killProcessByPort(int port) {
var pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>();
final dwSize = calloc<DWORD>();
dwSize.value = 0;
int result = _getExtendedTcpTable(
nullptr,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
if (result == ERROR_INSUFFICIENT_BUFFER) {
free(pTcpTable);
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
result = _getExtendedTcpTable(
pTcpTable,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
}
if (result == NO_ERROR) {
final table = pTcpTable.ref;
for (int i = 0; i < table.dwNumEntries; i++) {
final row = table.table[i];
final localPort = _htons(row.dwLocalPort);
if (localPort == port) {
final pid = row.dwOwningPid;
calloc.free(pTcpTable);
calloc.free(dwSize);
final hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (hProcess != NULL) {
final result = TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
return result != 0;
}
return false;
}
}
}
calloc.free(pTcpTable);
calloc.free(dwSize);
return false;
}
int _htons(int port) => ((port & 0xFF) << 8) | ((port >> 8) & 0xFF);

468
common/lib/src/util/os.dart Normal file
View File

@@ -0,0 +1,468 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:reboot_common/common.dart';
import 'package:win32/win32.dart';
import 'package:path/path.dart' as path;
bool useDefaultPath = false;
Directory get installationDirectory {
if(useDefaultPath) {
final dir = Directory('$_home/Reboot Launcher');
dir.createSync(recursive: true);
return dir;
}else {
return File(Platform.resolvedExecutable).parent;
}
}
String get _home {
if (Platform.isMacOS) {
return Platform.environment['HOME'] ?? '.';
} else if (Platform.isLinux) {
return Platform.environment['HOME'] ?? '.';
} else if (Platform.isWindows) {
return Platform.environment['UserProfile'] ?? '.';
}else {
return '.';
}
}
String? get antiVirusName {
final pLoc = calloc<COMObject>();
final rclsid = GUIDFromString(CLSID_WbemLocator);
final riid = GUIDFromString(IID_IWbemLocator);
final hr = CoCreateInstance(
rclsid,
nullptr,
CLSCTX.CLSCTX_INPROC_SERVER,
riid,
pLoc.cast(),
);
calloc.free(rclsid);
calloc.free(riid);
if (FAILED(hr)) {
return null;
}
final locator = IWbemLocator(pLoc);
final pSvc = calloc<COMObject>();
final scope = 'ROOT\\SecurityCenter2'.toNativeUtf16();
final hr2 = locator.connectServer(
scope,
nullptr,
nullptr,
nullptr,
0,
nullptr,
nullptr,
pSvc.cast()
);
calloc.free(scope);
if (FAILED(hr2)) {
return null;
}
final service = IWbemServices(pSvc);
final pEnumerator = calloc<COMObject>();
final wql = 'WQL'.toNativeUtf16();
final query = 'SELECT * FROM AntiVirusProduct'.toNativeUtf16();
final hr3 = service.execQuery(
wql,
query,
WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_FORWARD_ONLY | WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_RETURN_IMMEDIATELY,
nullptr,
pEnumerator.cast(),
);
calloc.free(wql);
calloc.free(query);
if (FAILED(hr3)) {
return null;
}
final enumerator = IEnumWbemClassObject(pEnumerator);
final uReturn = calloc<Uint32>();
final pClsObj = calloc<COMObject>();
final hr4 = enumerator.next(
WBEM_INFINITE,
1,
pClsObj.cast(),
uReturn,
);
String? result;
if (SUCCEEDED(hr4) && uReturn.value > 0) {
final clsObj = IWbemClassObject(pClsObj);
final vtProp = calloc<VARIANT>();
final propName = 'displayName'.toNativeUtf16();
final hr5 = clsObj.get(
propName,
0,
vtProp,
nullptr,
nullptr,
);
calloc.free(propName);
if (SUCCEEDED(hr5) && vtProp.ref.vt == VARENUM.VT_BSTR) {
final bstr = vtProp.ref.bstrVal;
result = bstr.toDartString();
}
calloc.free(vtProp);
}
calloc.free(uReturn);
return result;
}
String get defaultAntiVirusName => "Windows Defender";
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
Directory get assetsDirectory {
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
if(directory.existsSync()) {
return directory;
}
return installationDirectory;
}
Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings");
Directory get tempDirectory =>
Directory(Platform.environment["Temp"]!);
Future<bool> delete(FileSystemEntity file) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return Future.delayed(const Duration(seconds: 5)).then((value) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return false;
}
});
}
}
const _AF_INET = 2;
const _TCP_TABLE_OWNER_PID_LISTENER = 3;
final _getExtendedTcpTable = DynamicLibrary.open('iphlpapi.dll').lookupFunction<
Int32 Function(Pointer, Pointer<Uint32>, Int32, Int32, Int32, Int32),
int Function(Pointer, Pointer<Uint32>, int, int, int, int)>('GetExtendedTcpTable');
final class _MIB_TCPROW_OWNER_PID extends Struct {
@Uint32()
external int dwState;
@Uint32()
external int dwLocalAddr;
@Uint32()
external int dwLocalPort;
@Uint32()
external int dwRemoteAddr;
@Uint32()
external int dwRemotePort;
@Uint32()
external int dwOwningPid;
}
final class _MIB_TCPTABLE_OWNER_PID extends Struct {
@Uint32()
external int dwNumEntries;
@Array(512)
external Array<_MIB_TCPROW_OWNER_PID> table;
}
bool isLocalHost(String host) => host.trim() == "127.0.0.1"
|| host.trim().toLowerCase() == "localhost"
|| host.trim() == "0.0.0.0";
bool killProcessByPort(int port) {
var pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>();
final dwSize = calloc<DWORD>();
dwSize.value = 0;
int result = _getExtendedTcpTable(
nullptr,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
if (result == WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) {
calloc.free(pTcpTable);
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
result = _getExtendedTcpTable(
pTcpTable,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
}
if (result == NO_ERROR) {
final table = pTcpTable.ref;
for (int i = 0; i < table.dwNumEntries; i++) {
final row = table.table[i];
final localPort = _htons(row.dwLocalPort);
if (localPort == port) {
final pid = row.dwOwningPid;
calloc.free(pTcpTable);
calloc.free(dwSize);
final hProcess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_TERMINATE, FALSE, pid);
if (hProcess != NULL) {
final result = TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
return result != 0;
}
return false;
}
}
}
calloc.free(pTcpTable);
calloc.free(dwSize);
return false;
}
int _htons(int port) => ((port & 0xFF) << 8) | ((port >> 8) & 0xFF);
final _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreateRemoteThread = _kernel32.lookupFunction<
IntPtr Function(
IntPtr hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
IntPtr dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
Uint32 dwCreationFlags,
Pointer<Uint32> lpThreadId),
int Function(
int hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
int dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
int dwCreationFlags,
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
Future<void> injectDll(int pid, File file) async {
try {
await file.readAsBytes();
}catch(_) {
throw "${path.basename(file.path)} is not accessible";
}
final process = OpenProcess(0x43A, FALSE, pid);
final processAddress = GetProcAddress(
GetModuleHandle("KERNEL32".toNativeUtf16()),
"LoadLibraryA".toNativeUtf8()
);
if (processAddress == nullptr) {
throw "Cannot get process address for pid $pid";
}
final dllAddress = VirtualAllocEx(
process,
nullptr,
file.path.length + 1,
0x3000,
0x4
);
if(dllAddress == 0) {
throw "Cannot allocate memory for dll";
}
final writeMemoryResult = WriteProcessMemory(
process,
dllAddress,
file.path.toNativeUtf8(),
file.path.length,
nullptr
);
if (writeMemoryResult != 1) {
throw "Memory write failed";
}
final createThreadResult = _CreateRemoteThread(
process,
nullptr,
0,
processAddress,
dllAddress,
0,
nullptr
);
if (createThreadResult == -1) {
throw "Thread creation failed";
}
CloseHandle(process);
}
Future<bool> startElevatedProcess({required String executable, required String args, bool window = false}) async {
var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
return ShellExecuteEx(shellInput) == 1;
}
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
final argsOrEmpty = args ?? [];
final workingDirectory = _getWorkingDirectory(executable);
if(useTempBatch) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat");
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
await tempScriptFile.writeAsString(command, flush: true);
final process = await Process.start(
tempScriptFile.path,
[],
workingDirectory: workingDirectory,
environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
final process = await Process.start(
executable.path,
args ?? [],
workingDirectory: workingDirectory,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
String? _getWorkingDirectory(File executable) {
try {
log("[PROCESS] Calculating working directory for $executable");
final workingDirectory = executable.parent.resolveSymbolicLinksSync();
log("[PROCESS] Using working directory: $workingDirectory");
return workingDirectory;
}catch(error) {
log("[PROCESS] Cannot infer working directory: $error");
return null;
}
}
final _ntdll = DynamicLibrary.open('ntdll.dll');
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtResumeProcess');
final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess');
bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtSuspendProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtResumeProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
final class _ExtendedProcess implements Process {
final Process _delegate;
final Stream<List<int>>? _stdout;
final Stream<List<int>>? _stderr;
_ExtendedProcess(Process delegate, bool attached) :
_delegate = delegate,
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
@override
Future<int> get exitCode => _delegate.exitCode;
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
@override
int get pid => _delegate.pid;
@override
IOSink get stdin => _delegate.stdin;
@override
Stream<List<int>> get stdout {
final out = _stdout;
if(out == null) {
throw StateError("Output is not attached");
}
return out;
}
@override
Stream<List<int>> get stderr {
final err = _stderr;
if(err == null) {
throw StateError("Output is not attached");
}
return err;
}
}
Future<List<File>> findFiles(Directory directory, String name) => Isolate.run(() => directory.list(recursive: true, followLinks: true)
.handleError((_) {})
.where((event) => event is File && path.basename(event.path) == name)
.map((event) => event as File)
.toList());

View File

@@ -1,69 +0,0 @@
import 'dart:io';
import 'dart:typed_data';
final Uint8List _originalHeadless = Uint8List.fromList([
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
]);
final Uint8List _patchedHeadless = Uint8List.fromList([
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
]);
// Not used right now
final Uint8List _originalMatchmaking = Uint8List.fromList([
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
]);
final Uint8List _patchedMatchmaking = Uint8List.fromList([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
Future<bool> patchHeadless(File file) async =>
await _patch(file, _originalHeadless, _patchedHeadless);
Future<bool> patchMatchmaking(File file) async =>
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
try {
if(original.length != patched.length){
throw Exception("Cannot mutate length of binary file");
}
final source = await file.readAsBytes();
var readOffset = 0;
var patchOffset = -1;
var patchCount = 0;
while(readOffset < source.length){
if(source[readOffset] == original[patchCount]){
if(patchOffset == -1) {
patchOffset = readOffset;
}
if(readOffset - patchOffset + 1 == original.length) {
break;
}
patchCount++;
}else {
patchOffset = -1;
patchCount = 0;
}
readOffset++;
}
if(patchOffset == -1) {
return false;
}
for(var i = 0; i < patched.length; i++) {
source[patchOffset + i] = patched[i];
}
await file.writeAsBytes(source, flush: true);
return true;
}catch(_){
return false;
}
}

View File

@@ -1,37 +0,0 @@
import 'dart:io';
Directory get installationDirectory =>
File(Platform.resolvedExecutable).parent;
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
Directory get assetsDirectory {
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
if(directory.existsSync()) {
return directory;
}
return installationDirectory;
}
Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings");
Directory get tempDirectory =>
Directory(Platform.environment["Temp"]!);
Future<bool> delete(FileSystemEntity file) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return Future.delayed(const Duration(seconds: 5)).then((value) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return false;
}
});
}
}

View File

@@ -1,338 +0,0 @@
// ignore_for_file: non_constant_identifier_names
import 'dart:async';
import 'dart:collection';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:ffi/ffi.dart';
import 'package:reboot_common/common.dart';
import 'package:win32/win32.dart';
final _ntdll = DynamicLibrary.open('ntdll.dll');
final _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreateRemoteThread = _kernel32.lookupFunction<
IntPtr Function(
IntPtr hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
IntPtr dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
Uint32 dwCreationFlags,
Pointer<Uint32> lpThreadId),
int Function(
int hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
int dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
int dwCreationFlags,
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
const chunkSize = 1024;
Future<void> injectDll(int pid, File dll) async {
// Get the path to the file
final dllPath = dll.path;
final process = OpenProcess(
0x43A,
0,
pid
);
final processAddress = GetProcAddress(
GetModuleHandle("KERNEL32".toNativeUtf16()),
"LoadLibraryA".toNativeUtf8()
);
if (processAddress == nullptr) {
throw Exception("Cannot get process address for pid $pid");
}
final dllAddress = VirtualAllocEx(
process,
nullptr,
dllPath.length + 1,
0x3000,
0x4
);
final writeMemoryResult = WriteProcessMemory(
process,
dllAddress,
dllPath.toNativeUtf8(),
dllPath.length,
nullptr
);
if (writeMemoryResult != 1) {
throw Exception("Memory write failed");
}
final createThreadResult = _CreateRemoteThread(
process,
nullptr,
0,
processAddress,
dllAddress,
0,
nullptr
);
if (createThreadResult == -1) {
throw Exception("Thread creation failed");
}
final closeResult = CloseHandle(process);
if(closeResult != 1){
throw Exception("Cannot close handle");
}
}
Future<bool> startElevatedProcess({required String executable, required String args, bool window = false}) async {
var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
return ShellExecuteEx(shellInput) == 1;
}
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
final argsOrEmpty = args ?? [];
final workingDirectory = _getWorkingDirectory(executable);
if(useTempBatch) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat");
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
await tempScriptFile.writeAsString(command, flush: true);
final process = await Process.start(
tempScriptFile.path,
[],
workingDirectory: workingDirectory,
environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
final process = await Process.start(
executable.path,
args ?? [],
workingDirectory: workingDirectory,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
String? _getWorkingDirectory(File executable) {
try {
log("[PROCESS] Calculating working directory for $executable");
final workingDirectory = executable.parent.resolveSymbolicLinksSync();
log("[PROCESS] Using working directory: $workingDirectory");
return workingDirectory;
}catch(error) {
log("[PROCESS] Cannot infer working directory: $error");
return null;
}
}
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtResumeProcess');
final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess');
bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtSuspendProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtResumeProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
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) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
final args = LinkedHashMap<String, String>(
equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
hashCode: (a) => a.toUpperCase().hashCode
);
args.addAll({
"-epicapp": "Fortnite",
"-epicenv": "Prod",
"-epiclocale": "en-us",
"-epicportal": "",
"-skippatchcheck": "",
"-nobe": "",
"-fromfl": "eac",
"-fltoken": "3db3ba5dcbd2e16703f3978d",
"-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN": username,
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
"-AUTH_TYPE": "epic"
});
if(logging) {
args["-log"] = "";
}
if(host) {
args["-nosplash"] = "";
args["-nosound"] = "";
if(hostType == GameServerType.headless){
args["-nullrhi"] = "";
}
}
log("[PROCESS] Default args: $args");
log("[PROCESS] Adding custom args: $additionalArgs");
for(final additionalArg in additionalArgs.split(" ")) {
log("[PROCESS] Processing custom arg: $additionalArg");
final separatorIndex = additionalArg.indexOf("=");
final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex);
log("[PROCESS] Custom arg key: $argName");
final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1);
log("[PROCESS] Custom arg value: $argValue");
args[argName] = argValue;
log("[PROCESS] Updated args: $args");
}
log("[PROCESS] Final args result: $args");
return args.entries
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
.toList();
}
void handleGameOutput({
required String line,
required bool host,
required void Function() onDisplayAttached,
required void Function() onLoggedIn,
required void Function() onMatchEnd,
required void Function() onShutdown,
required void Function() onTokenError,
required void Function() onBuildCorrupted,
}) {
if (line.contains(kShutdownLine)) {
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd();
}else if(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;
}
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}
final class _ExtendedProcess implements Process {
final Process _delegate;
final Stream<List<int>>? _stdout;
final Stream<List<int>>? _stderr;
_ExtendedProcess(Process delegate, bool attached) :
_delegate = delegate,
_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);
}
}
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
@override
int get pid => _delegate.pid;
@override
IOSink get stdin => _delegate.stdin;
@override
Stream<List<int>> get stdout {
final out = _stdout;
if(out == null) {
throw StateError("Output is not attached");
}
return out;
}
@override
Stream<List<int>> get stderr {
final err = _stderr;
if(err == null) {
throw StateError("Output is not attached");
}
return err;
}
}

View File

@@ -19,6 +19,7 @@ dependencies:
uuid: ^4.5.1 uuid: ^4.5.1
shelf_web_socket: ^2.0.0 shelf_web_socket: ^2.0.0
version: ^3.0.2 version: ^3.0.2
synchronized: ^3.3.0+3
dev_dependencies: dev_dependencies:
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0

View File

@@ -1,5 +1,19 @@
# How can I make my server accessible to other players? # How can I make my server accessible to other players?
If you want other players to join your own server, you can either:
- Port forwarding
Port forwarding is a network technique that allows devices on the internet, like your friends' PC, to access devices your fortnite game server
running on your private WI-FI network. This is the better alternative in terms of latency as you don't have to pass through an external service.
- Use a private VPN software
Using a private VPN software, like Playit, Hamachi or Radmin, you can tunnel your internet traffic through a centralized VPN to connect with other players.
This allows you to not share your public IP with other players and can be easier to set up if you have never used your router's web interface.
# Option 1: Port Forwarding
### 1. Set a static IP ### 1. Set a static IP
Set a static IP on the PC hosting the game server and copy it for later: Set a static IP on the PC hosting the game server and copy it for later:
@@ -35,3 +49,7 @@ After configuring the port forwarding rule, save your changes and apply them.
This step may involve clicking a "Save" or "Apply" button on your router's web interface. This step may involve clicking a "Save" or "Apply" button on your router's web interface.
### 6. Try hosting a game! ### 6. Try hosting a game!
# OPTION 2: Private VPN software
I recommend using [Playit](https://playit.gg/) as it's the easiest to set up

View File

@@ -2,13 +2,13 @@
[OnlineSubsystemMcp.Xmpp] [OnlineSubsystemMcp.Xmpp]
bUseSSL=false bUseSSL=false
ServerAddr="ws://127.0.0.1" ServerAddr="ws://127.0.0.1"
ServerPort=80 ServerPort=8080
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp # Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp
[OnlineSubsystemMcp.Xmpp Prod] [OnlineSubsystemMcp.Xmpp Prod]
bUseSSL=false bUseSSL=false
ServerAddr="ws://127.0.0.1" ServerAddr="ws://127.0.0.1"
ServerPort=80 ServerPort=8080
# Forces fortnite to use the v1 party system to support lawinserver xmpp # Forces fortnite to use the v1 party system to support lawinserver xmpp
[OnlineSubsystemMcp] [OnlineSubsystemMcp]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 0 B

View File

@@ -63,10 +63,24 @@
"favorite": false "favorite": false
}, },
{ {
"accountId": "Player231", "accountId": "Player809",
"status": "ACCEPTED", "status": "ACCEPTED",
"direction": "OUTBOUND", "direction": "OUTBOUND",
"created": "2024-05-23T20:05:07.478Z", "created": "2024-05-31T19:09:47.089Z",
"favorite": false
},
{
"accountId": "Player153",
"status": "ACCEPTED",
"direction": "OUTBOUND",
"created": "2024-05-31T19:50:04.738Z",
"favorite": false
},
{
"accountId": "Player724",
"status": "ACCEPTED",
"direction": "OUTBOUND",
"created": "2024-06-24T20:15:48.062Z",
"favorite": false "favorite": false
} }
] ]

View File

@@ -82,13 +82,31 @@
"created": "2024-05-23T19:36:22.635Z" "created": "2024-05-23T19:36:22.635Z"
}, },
{ {
"accountId": "Player231", "accountId": "Player809",
"groups": [], "groups": [],
"mutual": 0, "mutual": 0,
"alias": "", "alias": "",
"note": "", "note": "",
"favorite": false, "favorite": false,
"created": "2024-05-23T20:05:07.478Z" "created": "2024-05-31T19:09:47.089Z"
},
{
"accountId": "Player153",
"groups": [],
"mutual": 0,
"alias": "",
"note": "",
"favorite": false,
"created": "2024-05-31T19:50:04.738Z"
},
{
"accountId": "Player724",
"groups": [],
"mutual": 0,
"alias": "",
"note": "",
"favorite": false,
"created": "2024-06-24T20:15:48.062Z"
} }
], ],
"incoming": [], "incoming": [],

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -128,10 +128,11 @@
"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": "Import",
"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",
"downloadVersion": "Download",
"cannotUpdateGameServer": "An error occurred while updating the game server: {error}", "cannotUpdateGameServer": "An error occurred while updating the game server: {error}",
"launchFortnite": "Launch Fortnite", "launchFortnite": "Launch Fortnite",
"closeFortnite": "Close Fortnite", "closeFortnite": "Close Fortnite",
@@ -146,9 +147,9 @@
"defaultServerName": "Reboot Game Server", "defaultServerName": "Reboot Game Server",
"defaultServerDescription": "Just another server", "defaultServerDescription": "Just another server",
"downloadingDll": "Downloading {name} dll...", "downloadingDll": "Downloading {name} dll...",
"dllAlreadyExists": "The {name} was already downloaded",
"downloadDllSuccess": "The {name} dll was downloaded successfully", "downloadDllSuccess": "The {name} dll was downloaded successfully",
"downloadDllError": "An error occurred while downloading {name}: {error}", "downloadDllError": "An error occurred while downloading {name}: {error}",
"downloadDllAntivirus": "The {name} dll was deleted: your antivirus({antivirus}) might have flagged it",
"downloadDllRetry": "Retry", "downloadDllRetry": "Retry",
"uncaughtErrorMessage": "An uncaught error was thrown: {error}", "uncaughtErrorMessage": "An uncaught error was thrown: {error}",
"launchingGameServer": "Launching the game server...", "launchingGameServer": "Launching the game server...",
@@ -212,6 +213,7 @@
"selectBuild": "Select a fortnite version", "selectBuild": "Select a fortnite version",
"fetchingBuilds": "Fetching builds and disks...", "fetchingBuilds": "Fetching builds and disks...",
"unknownError": "Unknown error", "unknownError": "Unknown error",
"unknown": "unknown",
"downloadVersionError": "Cannot download version: {error}", "downloadVersionError": "Cannot download version: {error}",
"downloadedVersion": "The download was completed successfully!", "downloadedVersion": "The download was completed successfully!",
"download": "Download", "download": "Download",
@@ -241,8 +243,8 @@
"gameServerStarted": "The game server was started successfully", "gameServerStarted": "The game server was started successfully",
"gameClientStarted": "The game client was started successfully", "gameClientStarted": "The game client was started successfully",
"checkingGameServer": "Checking if other players can join the game server...", "checkingGameServer": "Checking if other players can join the game server...",
"checkGameServerFixMessage": "Other players can't join the game server as port {port} isn't open", "checkGameServerFixMessage": "The game server was started successfully, but other players can't join yet as port {port} isn't open",
"checkGameServerFixAction": "Fix", "checkGameServerFixAction": "Learn more",
"infoName": "Info", "infoName": "Info",
"emptyVersionName": "Empty version name", "emptyVersionName": "Empty version name",
"versionAlreadyExists": "This version already exists", "versionAlreadyExists": "This version already exists",
@@ -258,7 +260,8 @@
"emptyURL": "Empty update URL", "emptyURL": "Empty update URL",
"missingVersionError": "Download or select a version before starting Fortnite", "missingVersionError": "Download or select a version before starting Fortnite",
"missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted", "missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted",
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version", "multipleExecutablesError": "There must be only one executable named {name} in the game directory",
"corruptedVersionError": "Fortnite crashed while starting: either the game installation is corrupted or an injected dll({dlls}) tried to access memory illegally",
"corruptedDllError": "Cannot inject dll: {error}", "corruptedDllError": "Cannot inject dll: {error}",
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings", "missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})", "tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
@@ -282,9 +285,9 @@
"infoVideoName": "Tutorial", "infoVideoName": "Tutorial",
"infoVideoDescription": "Show the tutorial again in the launcher", "infoVideoDescription": "Show the tutorial again in the launcher",
"infoVideoContent": "Start Tutorial", "infoVideoContent": "Start Tutorial",
"dllDeletedTitle": "A critical dll was deleted. If you didn't delete it, your Antivirus probably flagged it. This is a false positive: please disable your Antivirus and try again", "dllDeletedTitle": "A critical dll was deleted and couldn't be reinstalled",
"dllDeletedSecondaryAction": "Close", "dllDeletedSecondaryAction": "Close",
"dllDeletedPrimaryAction": "Try again", "dllDeletedPrimaryAction": "Disable Antivirus",
"clickKey": "Waiting for a key to be registered", "clickKey": "Waiting for a key to be registered",
"settingsLogsName": "Export logs", "settingsLogsName": "Export logs",
"settingsLogsDescription": "Exports an archive containing all the logs produced by the launcher", "settingsLogsDescription": "Exports an archive containing all the logs produced by the launcher",
@@ -306,11 +309,8 @@
"quizZeroTriesLeft": "zero tries", "quizZeroTriesLeft": "zero tries",
"quizOneTryLeft": "one try", "quizOneTryLeft": "one try",
"quizTwoTriesLeft": "two tries", "quizTwoTriesLeft": "two tries",
"gameServerTypeName": "Type", "gameServerTypeName": "Headless",
"gameServerTypeDescription": "The type of game server to use", "gameServerTypeDescription": "Disables game rendering to save resources",
"gameServerTypeHeadless": "Background process",
"gameServerTypeVirtualWindow": "Virtual window",
"gameServerTypeWindow": "Normal window",
"localBuild": "This PC", "localBuild": "This PC",
"githubArchive": "Cloud archive", "githubArchive": "Cloud archive",
"all": "All", "all": "All",
@@ -374,5 +374,14 @@
"gameResetDefaultsDescription": "Resets the game's settings to their default values", "gameResetDefaultsDescription": "Resets the game's settings to their default values",
"gameResetDefaultsContent": "Reset", "gameResetDefaultsContent": "Reset",
"selectFile": "Select a file", "selectFile": "Select a file",
"reset": "Reset" "reset": "Reset",
"importingVersion": "Looking for Fortnite game files...",
"importedVersion": "Successfully imported version",
"importVersionMissingShippingExeError": "Cannot import version: {name} should exist in the directory",
"importVersionMultipleShippingExesError": "Cannot import version: only one {name} should exist in the directory",
"importVersionUnsupportedVersionError": "This version of Fortnite is not supported by the launcher",
"downloadManually": "Download manually",
"gameServerPortEqualsBackendPort": "The game server port cannot be {backendPort} as its reserved for the backend",
"gameServer": "game server",
"client": "client"
} }

View File

@@ -21,8 +21,10 @@ import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'hosting_controller.dart';
class BackendController extends GetxController { class BackendController extends GetxController {
static const String storageName = "v2_backend_storage"; static const String storageName = "v3_backend_storage";
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage? _storage; late final GetStorage? _storage;
@@ -35,10 +37,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 +48,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 +143,36 @@ 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(); if(started.value) {
stop(interactive: false);
Get.find<GameController>()
.instance
.value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
_showRebootInfoBar( _showRebootInfoBar(
translations.backendErrorMessage, translations.backendErrorMessage,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
@@ -170,269 +183,207 @@ 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:
print(event.stackTrace); if(interactive) {
return _showRebootInfoBar( return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError), type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
}else {
return null;
}
case ServerResultType.stopping: case ServerResultType.stopping:
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;
} }
} }
@@ -447,7 +398,7 @@ class BackendController extends GetxController {
} }
final version = Get.find<GameController>() final version = Get.find<GameController>()
.getVersionByName(server.version.toString()); .getVersionByGame(server.version.toString());
if(version == null) { if(version == null) {
_showRebootInfoBar( _showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()), translations.cannotJoinServerVersion(server.version.toString()),
@@ -569,8 +520,7 @@ class BackendController extends GetxController {
}else { }else {
FlutterClipboard.controlC(decryptedIp); FlutterClipboard.controlC(decryptedIp);
} }
Get.find<GameController>() Get.find<GameController>().selectedVersion.value = version;
.selectedVersion = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar( WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp, embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration, duration: infoBarLongDuration,
@@ -598,4 +548,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

@@ -9,15 +9,17 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
class DllController extends GetxController { class DllController extends GetxController {
static const String storageName = "v2_dll_storage"; static const String storageName = "v3_dll_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String originalDll; late final TextEditingController customGameServerDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll; late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll; late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll; late final TextEditingController memoryLeakDll;
@@ -28,11 +30,11 @@ 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; late final Map<InjectableDll, StreamSubscription?> _subscriptions;
DllController() { DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName); _storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.gameServer); customGameServerDll = _createController("game_server", InjectableDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console); unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.auth); backendDll = _createController("backend", InjectableDll.auth);
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak); memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
@@ -41,15 +43,16 @@ class DllController extends GetxController {
final timerIndex = _storage?.read("timer"); final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex)); timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index)); timer.listen((value) => _storage?.write("timer", value.index));
beforeS20Mirror = TextEditingController(text: _storage?.read("update_url") ?? kRebootBelowS20DownloadUrl); beforeS20Mirror = TextEditingController(text: _storage?.read("before_s20_update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text)); beforeS20Mirror.addListener(() => _storage?.write("before_s20_update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl); aboveS20Mirror = TextEditingController(text: _storage?.read("after_s20_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text)); aboveS20Mirror.addListener(() => _storage?.write("after_s20_update_url", aboveS20Mirror.text));
status = Rx(UpdateStatus.waiting); status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false); customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value)); customGameServer.listen((value) => _storage?.write("custom_game_server", value));
timestamp = RxnInt(_storage?.read("ts")); timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value)); timestamp.listen((value) => _storage?.write("ts", value));
_subscriptions = {};
} }
TextEditingController _createController(String key, InjectableDll dll) { TextEditingController _createController(String key, InjectableDll dll) {
@@ -59,7 +62,7 @@ class DllController extends GetxController {
} }
void resetGame() { void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.gameServer); customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console); unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.auth); backendDll.text = getDefaultDllPath(InjectableDll.auth);
} }
@@ -76,9 +79,11 @@ 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;
_listenToFileEvents(InjectableDll.gameServer);
return true; return true;
} }
@@ -89,6 +94,7 @@ class DllController extends GetxController {
); );
if(!needsUpdate) { if(!needsUpdate) {
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true; return true;
} }
@@ -99,14 +105,25 @@ class DllController extends GetxController {
duration: null duration: null
); );
} }
await Future.wait( final result = 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))
.then((_) => true)
], ],
eagerError: false eagerError: false
).then((values) => values.reduce((first, second) => first && second));
if(!result) {
status.value = UpdateStatus.error;
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, "reboot"),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
); );
infoBarEntry?.close();
return false;
}
timestamp.value = DateTime.now().millisecondsSinceEpoch; timestamp.value = DateTime.now().millisecondsSinceEpoch;
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
infoBarEntry?.close(); infoBarEntry?.close();
@@ -117,6 +134,7 @@ class DllController extends GetxController {
duration: infoBarShortDuration duration: infoBarShortDuration
); );
} }
_listenToFileEvents(InjectableDll.gameServer);
return true; return true;
}catch(message) { }catch(message) {
infoBarEntry?.close(); infoBarEntry?.close();
@@ -124,34 +142,37 @@ class DllController extends GetxController {
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase(); error = error.toLowerCase();
status.value = UpdateStatus.error; status.value = UpdateStatus.error;
final completer = Completer<bool>();
infoBarEntry = showRebootInfoBar( infoBarEntry = showRebootInfoBar(
translations.downloadDllError(error.toString(), "reboot.dll"), translations.downloadDllError(error.toString(), "reboot.dll"),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(false),
action: Button( action: Button(
onPressed: () async { onPressed: () async {
infoBarEntry?.close(); infoBarEntry?.close();
updateGameServerDll( final result = updateGameServerDll(
force: true, force: true,
silent: silent silent: silent
); );
completer.complete(result);
}, },
child: Text(translations.downloadDllRetry), child: Text(translations.downloadDllRetry),
) )
); );
return false; return completer.future;
} }
} }
(File, bool) getInjectableData(Version version, InjectableDll dll) { (File, bool) getInjectableData(String version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll)); final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){ switch(dll){
case InjectableDll.gameServer: case InjectableDll.gameServer:
if(customGameServer.value) { if(customGameServer.value) {
return (File(gameServerDll.text), true); return (File(customGameServerDll.text), true);
} }
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false); return (_isS20(version) ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
case InjectableDll.console: case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text); final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath); return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
@@ -164,6 +185,14 @@ class DllController extends GetxController {
} }
} }
bool _isS20(String version) {
try {
return Version.parse(version).major >= 20;
} on FormatException catch(_) {
return version.trim().startsWith("20.");
}
}
TextEditingController getDllEditingController(InjectableDll dll) { TextEditingController getDllEditingController(InjectableDll dll) {
switch(dll) { switch(dll) {
case InjectableDll.console: case InjectableDll.console:
@@ -171,7 +200,7 @@ class DllController extends GetxController {
case InjectableDll.auth: case InjectableDll.auth:
return backendDll; return backendDll;
case InjectableDll.gameServer: case InjectableDll.gameServer:
return gameServerDll; return customGameServerDll;
case InjectableDll.memoryLeak: case InjectableDll.memoryLeak:
return memoryLeakDll; return memoryLeakDll;
} }
@@ -182,7 +211,7 @@ class DllController extends GetxController {
case InjectableDll.console: case InjectableDll.console:
return "${dllsDirectory.path}\\console.dll"; return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth: case InjectableDll.auth:
return "${dllsDirectory.path}\\starfall.dll"; return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer: case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll"; return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak: case InjectableDll.memoryLeak:
@@ -199,77 +228,136 @@ class DllController extends GetxController {
} }
if(!force && File(filePath).existsSync()) { if(!force && File(filePath).existsSync()) {
log("[DLL] File already exists"); log("[DLL] $dll already exists");
_listenToFileEvents(dll);
return true; return true;
} }
log("[DLL] Downloading $dll...");
final fileNameWithoutExtension = basenameWithoutExtension(filePath); final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) { if(!silent) {
log("[DLL] Showing dialog while downloading $dll...");
entry = showRebootInfoBar( entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension), translations.downloadingDll(fileNameWithoutExtension),
loading: true, loading: true,
duration: null duration: null
); );
}else {
log("[DLL] Not showing dialog while downloading $dll...");
} }
await downloadDependency(dll, filePath); final result = await downloadDependency(dll, filePath);
if(!result) {
entry?.close();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
log("[DLL] Downloaded $dll");
entry?.close(); entry?.close();
if(!silent) { if(!silent) {
log("[DLL] Showing success dialog for $dll");
entry = await showRebootInfoBar( entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension), translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
); );
}else {
log("[DLL] Not showing success dialog for $dll");
} }
_listenToFileEvents(dll);
return true; return true;
}catch(message) { }catch(message) {
log("[DLL] Error: $message"); log("[DLL] An error occurred while downloading $dll: $message");
entry?.close(); entry?.close();
var error = message.toString(); var error = message.toString();
error = error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase(); error = error.toLowerCase();
final completer = Completer(); final completer = Completer<bool>();
await showRebootInfoBar( await showRebootInfoBar(
translations.downloadDllError(error.toString(), dll.name), translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null), onDismissed: () => completer.complete(false),
action: Button( action: Button(
onPressed: () async { onPressed: () async {
await download(dll, filePath, silent: silent, force: force); final result = await download(dll, filePath, silent: silent, force: force);
completer.complete(null); completer.complete(result);
}, },
child: Text(translations.downloadDllRetry), child: Text(translations.downloadDllRetry),
) )
); );
await completer.future; return completer.future;
return false;
} }
} }
void guardFiles() { Future<void> downloadAndGuardDependencies() async {
for(final injectable in InjectableDll.values) { for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable); final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable); final defaultPath = getDefaultDllPath(injectable);
if (path.equals(controller.text, defaultPath)) {
download(injectable, controller.text); if(path.equals(controller.text, defaultPath)) {
await download(injectable, controller.text);
} }
controller.addListener(() async { }
try { }
if (!path.equals(controller.text, defaultPath)) {
void _listenToFileEvents(InjectableDll injectable) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
void onFileEvent(FileSystemEvent event, String filePath) {
if (!path.equals(event.path, filePath)) {
return; return;
} }
final filePath = controller.text; if(path.equals(filePath, defaultPath)) {
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) { Get.find<GameController>()
if (path.equals(event.path, filePath)) { .instance
await download(injectable, filePath); .value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
} }
_updateInput(injectable);
} }
} catch(_) {
// Ignore StreamSubscription subscribe(String filePath) => File(filePath)
} .parent
.watch(events: FileSystemEvent.delete | FileSystemEvent.move)
.listen((event) => onFileEvent(event, filePath));
controller.addListener(() {
_subscriptions[injectable]?.cancel();
_subscriptions[injectable] = subscribe(controller.text);
}); });
_subscriptions[injectable] = subscribe(controller.text);
}
void _updateInput(InjectableDll injectable) {
switch(injectable) {
case InjectableDll.console:
settingsConsoleDllInputKey.currentState?.validate();
break;
case InjectableDll.auth:
settingsAuthDllInputKey.currentState?.validate();
break;
case InjectableDll.gameServer:
settingsGameServerDllInputKey.currentState?.validate();
break;
case InjectableDll.memoryLeak:
settingsMemoryDllInputKey.currentState?.validate();
break;
} }
} }
} }

View File

@@ -1,21 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:version/version.dart';
class GameController extends GetxController { class GameController extends GetxController {
static const String storageName = "v2_game_storage"; static const String storageName = "v3_game_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final TextEditingController username; late final TextEditingController username;
late final TextEditingController password; late final TextEditingController password;
late final TextEditingController customLaunchArgs; late final TextEditingController customLaunchArgs;
late final Rx<List<FortniteVersion>> versions; late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion; late final Rxn<FortniteVersion> selectedVersion;
late final RxBool started; late final RxBool started;
late final Rxn<GameInstance> instance; late final Rxn<GameInstance> instance;
@@ -28,8 +30,8 @@ class GameController extends GetxController {
versions = Rx(decodedVersions); versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions()); versions.listen((data) => _saveVersions());
final decodedSelectedVersionName = _storage?.read("version"); final decodedSelectedVersionName = _storage?.read("version");
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName); selectedVersion = Rxn(decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName));
_selectedVersion = Rxn(decodedSelectedVersion); selectedVersion.listen((version) => _storage?.write("version", version?.name));
username = TextEditingController( username = TextEditingController(
text: _storage?.read("username") ?? kDefaultPlayerName); text: _storage?.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage?.write("username", username.text)); username.addListener(() => _storage?.write("username", username.text));
@@ -46,26 +48,42 @@ class GameController extends GetxController {
password.text = ""; password.text = "";
customLaunchArgs.text = ""; customLaunchArgs.text = "";
versions.value = []; versions.value = [];
_selectedVersion.value = null; selectedVersion.value = null;
instance.value = null; instance.value = null;
} }
FortniteVersion? getVersionByName(String name) { FortniteVersion? getVersionByName(String name) {
return versions.value.firstWhereOrNull((element) => element.content.toString() == name); name = name.trim();
return versions.value.firstWhereOrNull((element) => element.name == name);
}
FortniteVersion? getVersionByGame(String gameVersion) {
gameVersion = gameVersion.trim();
final parsedGameVersion = Version.parse(gameVersion);
return versions.value.firstWhereOrNull((element) {
final compare = element.gameVersion.trim();
try {
final parsedCompare = Version.parse(compare);
return parsedCompare.major == parsedGameVersion.major
&& parsedCompare.minor == parsedGameVersion.minor;
} on FormatException {
return compare == gameVersion;
}
});
} }
void addVersion(FortniteVersion version) { void addVersion(FortniteVersion version) {
var empty = versions.value.isEmpty;
versions.update((val) => val?.add(version)); versions.update((val) => val?.add(version));
if(empty){ selectedVersion.value = version;
selectedVersion = version;
}
} }
void removeVersion(FortniteVersion version) { void removeVersion(FortniteVersion version) {
versions.update((val) => val?.remove(version)); final index = versions.value.indexOf(version);
if (selectedVersion == version || hasNoVersions) { versions.update((val) => val?.removeAt(index));
selectedVersion = null; if(hasNoVersions) {
selectedVersion.value = null;
}else {
selectedVersion.value = versions.value.elementAt(max(0, index - 1));
} }
} }
@@ -78,14 +96,5 @@ class GameController extends GetxController {
bool get hasNoVersions => versions.value.isEmpty; bool get hasNoVersions => versions.value.isEmpty;
FortniteVersion? get selectedVersion => _selectedVersion(); void updateVersion(FortniteVersion version, Function(FortniteVersion) function) => versions.update((val) => function(version));
set selectedVersion(FortniteVersion? version) {
_selectedVersion.value = version;
_storage?.write("version", version?.content.toString());
}
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
versions.update((val) => function(version));
}
} }

View File

@@ -12,7 +12,7 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class HostingController extends GetxController { class HostingController extends GetxController {
static const String storageName = "v2_hosting_storage"; static const String storageName = "v3_hosting_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String uuid; late final String uuid;
@@ -26,7 +26,7 @@ class HostingController extends GetxController {
late final FocusNode passwordFocusNode; late final FocusNode passwordFocusNode;
late final RxBool showPassword; late final RxBool showPassword;
late final RxBool discoverable; late final RxBool discoverable;
late final Rx<GameServerType> type; late final RxBool headless;
late final RxBool autoRestart; late final RxBool autoRestart;
late final RxBool started; late final RxBool started;
late final RxBool published; late final RxBool published;
@@ -54,8 +54,8 @@ class HostingController extends GetxController {
passwordFocusNode = FocusNode(); passwordFocusNode = FocusNode();
discoverable = RxBool(_storage?.read("discoverable") ?? false); discoverable = RxBool(_storage?.read("discoverable") ?? false);
discoverable.listen((value) => _storage?.write("discoverable", value)); discoverable.listen((value) => _storage?.write("discoverable", value));
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index)); headless = RxBool(_storage?.read("headless") ?? true);
type.listen((value) => _storage?.write("type", value.index)); headless.listen((value) => _storage?.write("headless", value));
autoRestart = RxBool(_storage?.read("auto_restart") ?? true); autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
autoRestart.listen((value) => _storage?.write("auto_restart", value)); autoRestart.listen((value) => _storage?.write("auto_restart", value));
started = RxBool(false); started = RxBool(false);
@@ -165,7 +165,7 @@ class HostingController extends GetxController {
showPassword.value = false; showPassword.value = false;
discoverable.value = false; discoverable.value = false;
instance.value = null; instance.value = null;
type.value = GameServerType.headless; headless.value = true;
autoRestart.value = true; autoRestart.value = true;
customLaunchArgs.text = ""; customLaunchArgs.text = "";
} }

View File

@@ -13,7 +13,7 @@ import 'package:version/version.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
class SettingsController extends GetxController { class SettingsController extends GetxController {
static const String storageName = "v2_settings_storage"; static const String storageName = "v3_settings_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final RxString language; late final RxString language;

View File

@@ -126,7 +126,7 @@ class ProgressDialog extends AbstractDialog {
header: InfoLabel( header: InfoLabel(
label: text, label: text,
child: Container( child: Container(
padding: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
width: double.infinity, width: double.infinity,
child: const ProgressBar() child: const ProgressBar()
), ),

View File

@@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4);
const infoBarShortDuration = Duration(seconds: 2); const infoBarShortDuration = Duration(seconds: 2);
const _height = 64.0; const _height = 64.0;
InfoBarEntry showRebootInfoBar(dynamic text, { InfoBarEntry showRebootInfoBar(String text, {
InfoBarSeverity severity = InfoBarSeverity.info, InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false, bool loading = false,
Duration? duration = infoBarShortDuration, Duration? duration = infoBarShortDuration,

View File

@@ -47,448 +47,6 @@ Future<String?> openFilePicker(String extension) async {
bool get isDarkMode => bool get isDarkMode =>
SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark; SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
class _ServiceProvider10 extends IUnknown {
static const String _CLSID = "{C2F03A33-21F5-47FA-B4BB-156362A2F239}";
static const String _IID = "{6D5140C1-7436-11CE-8034-00AA006009FA}";
_ServiceProvider10._internal(Pointer<COMObject> ptr) : super(ptr);
factory _ServiceProvider10.createInstance() =>
_ServiceProvider10._internal(COMObject.createFromID(_CLSID, _IID));
Pointer<COMObject> queryService(String classId, String instanceId) {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<GUID>, Pointer<GUID>,
Pointer<COMObject>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<GUID>, Pointer<GUID>,
Pointer<COMObject>)>()(ptr.ref.lpVtbl,
GUIDFromString(classId), GUIDFromString(instanceId), result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result;
}
}
class IVirtualDesktop extends IUnknown {
static const String _CLSID = "{3F07F4BE-B107-441A-AF0F-39D82529072C}";
IVirtualDesktop._internal(super.ptr);
String getName() {
final result = calloc<HSTRING>();
final code = (ptr.ref.vtable + 5)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<HSTRING>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<HSTRING>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return _convertFromHString(result.value);
}
}
class IApplicationView extends IUnknown {
// static const String _CLSID = "{372E1D3B-38D3-42E4-A15B-8AB2B178F513}";
IApplicationView._internal(super.ptr);
}
class _IObjectArray extends IUnknown {
_IObjectArray(super.ptr);
int getCount() {
final result = calloc<Int32>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result.value;
}
Pointer<COMObject> getAt(int index, String guid) {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 4)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Int32 index, Pointer<GUID>,
Pointer<COMObject>)>>>()
.value
.asFunction<
int Function(
Pointer, int index, Pointer<GUID>, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, index, GUIDFromString(guid), result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result;
}
}
typedef _IObjectMapper<T> = T Function(Pointer<COMObject>);
class _IObjectArrayList<T> extends ListBase<T> {
final _IObjectArray _array;
final String _guid;
final _IObjectMapper<T> _mapper;
_IObjectArrayList(
{required _IObjectArray array,
required String guid,
required _IObjectMapper<T> mapper})
: _array = array,
_guid = guid,
_mapper = mapper;
@override
int get length => _array.getCount();
@override
set length(int newLength) {
throw UnsupportedError("Immutable list");
}
@override
T operator [](int index) => _mapper(_array.getAt(index, _guid));
@override
void operator []=(int index, T value) {
throw UnsupportedError("Immutable list");
}
}
class _IVirtualDesktopManagerInternal extends IUnknown {
static const String _CLSID = "{C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B}";
static const String _IID_WIN10 = "{F31574D6-B682-4CDC-BD56-1827860ABEC6}";
static const String _IID_WIN_21H2 = "{B2F925B9-5A0F-4D2E-9F4D-2B1507593C10}";
static const String _IID_WIN_23H2 = "{A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E}";
static const String _IID_WIN_23H2_3085 = "{53F5CA0B-158F-4124-900C-057158060B27}";
_IVirtualDesktopManagerInternal._internal(super.ptr);
int getDesktopsCount() {
final result = calloc<Int32>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result.value;
}
List<IVirtualDesktop> getDesktops() {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 7)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
final array = _IObjectArray(result);
return _IObjectArrayList(
array: array,
guid: IVirtualDesktop._CLSID,
mapper: (comObject) => IVirtualDesktop._internal(comObject));
}
void moveWindowToDesktop(IApplicationView view, IVirtualDesktop desktop) {
final code = (ptr.ref.vtable + 4)
.cast<
Pointer<
NativeFunction<
Int32 Function(Pointer, COMObject, COMObject)>>>()
.value
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
ptr.ref.lpVtbl, view.ptr.ref, desktop.ptr.ref);
if (code != 0) {
throw WindowsException(code, message: "Cannot move window");
}
}
IVirtualDesktop createDesktop() {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 10)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return IVirtualDesktop._internal(result);
}
void removeDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback) {
final code = (ptr.ref.vtable + 12)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, COMObject, COMObject)>>>()
.value
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, fallback.ptr.ref);
if (code != 0) {
throw WindowsException(code);
}
}
void setDesktopName(IVirtualDesktop desktop, String newName) {
final code =
(ptr.ref.vtable + 15)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, COMObject, Int8)>>>()
.value
.asFunction<int Function(Pointer, COMObject, int)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
if (code != 0) {
throw WindowsException(code);
}
}
}
class _IApplicationViewCollection extends IUnknown {
static const String _CLSID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
static const String _IID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
_IApplicationViewCollection._internal(super.ptr);
IApplicationView? getViewForHWnd(int HWnd) {
final result = calloc<COMObject>();
final code =
(ptr.ref.vtable + 6)
.cast<
Pointer<
NativeFunction<
HRESULT Function(
Pointer, IntPtr, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, int, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, HWnd, result);
if (code != 0) {
free(result);
return null;
}
return IApplicationView._internal(result);
}
}
final class Win32Process extends Struct {
@Uint32()
external int pid;
@Uint32()
external int HWndLength;
external Pointer<Uint32> HWnd;
external Pointer<Utf16> excluded;
}
int _filter(int HWnd, int lParam) {
final structure = Pointer.fromAddress(lParam).cast<Win32Process>();
if(structure.ref.excluded != nullptr) {
final excludedWindowName = structure.ref.excluded.toDartString();
final windowNameLength = GetWindowTextLength(HWnd);
if(windowNameLength > 0) {
final windowNamePointer = calloc<Uint16>(windowNameLength + 1).cast<Utf16>();
GetWindowText(HWnd, windowNamePointer, windowNameLength);
final windowName = windowNamePointer.toDartString(length: windowNameLength);
if(windowName.toLowerCase().contains(excludedWindowName.toLowerCase())) {
return TRUE;
}
}
}
final pidPointer = calloc<Uint32>();
GetWindowThreadProcessId(HWnd, pidPointer);
final pid = pidPointer.value;
if (pid == structure.ref.pid) {
final length = structure.ref.HWndLength;
final newLength = length + 1;
final ptr = malloc.allocate<Uint32>(sizeOf<Uint32>() * newLength);
final list = structure.ref.HWnd.asTypedList(length);
for (var i = 0; i < list.length; i++) {
(ptr + i).value = list[i];
}
ptr[list.length] = HWnd;
structure.ref.HWndLength = newLength;
free(structure.ref.HWnd);
structure.ref.HWnd = ptr;
}
free(pidPointer);
return TRUE;
}
List<int> _getHWnds(int pid, String? excludedWindowName) {
final result = calloc<Win32Process>();
result.ref.pid = pid;
if(excludedWindowName != null) {
result.ref.excluded = excludedWindowName.toNativeUtf16();
}
EnumWindows(Pointer.fromFunction<WNDENUMPROC>(_filter, TRUE), result.address);
final length = result.ref.HWndLength;
final HWndsPointer = result.ref.HWnd;
if(HWndsPointer == nullptr) {
calloc.free(result);
return [];
}
final HWnds = HWndsPointer.asTypedList(length)
.toList(growable: false);
calloc.free(result);
return HWnds;
}
class VirtualDesktopManager {
static VirtualDesktopManager? _instance;
final _IVirtualDesktopManagerInternal windowManager;
final _IApplicationViewCollection applicationViewCollection;
VirtualDesktopManager._internal(this.windowManager, this.applicationViewCollection);
factory VirtualDesktopManager.getInstance() {
if (_instance != null) {
return _instance!;
}
final hr = CoInitializeEx(
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
if (FAILED(hr)) {
throw WindowsException(hr);
}
final shell = _ServiceProvider10.createInstance();
final windowManager = _createWindowManager(shell);
final applicationViewCollection = _IApplicationViewCollection._internal(
shell.queryService(_IApplicationViewCollection._CLSID,
_IApplicationViewCollection._IID));
return _instance =
VirtualDesktopManager._internal(windowManager, applicationViewCollection);
}
static _IVirtualDesktopManagerInternal _createWindowManager(_ServiceProvider10 shell) {
final build = windowsBuild;
if(build == null || build < 19044) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN10));
}else if(build >= 19044 && build < 22631) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_21H2));
}else if(build >= 22631 && build < 22631) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_23H2));
}else {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_23H2_3085));
}
}
int getDesktopsCount() => windowManager.getDesktopsCount();
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
Future<bool> moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1), int remainingPolls = 10, String? excludedWindowName}) async {
for(final hWND in _getHWnds(pid, excludedWindowName)) {
final window = applicationViewCollection.getViewForHWnd(hWND);
if(window != null) {
windowManager.moveWindowToDesktop(window, desktop);
return true;
}
}
if(remainingPolls <= 0) {
return false;
}
await Future.delayed(pollTime);
return await moveWindowToDesktop(
pid,
desktop,
pollTime: pollTime,
remainingPolls: remainingPolls - 1
);
}
IVirtualDesktop createDesktop() => windowManager.createDesktop();
void removeDesktop(IVirtualDesktop desktop, [IVirtualDesktop? fallback]) {
fallback ??= getDesktops().first;
return windowManager.removeDesktop(desktop, fallback);
}
void setDesktopName(IVirtualDesktop desktop, String newName) =>
windowManager.setDesktopName(desktop, newName);
}
String _convertFromHString(int hstring) =>
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
int _convertToHString(String string) {
final hString = calloc<HSTRING>();
final stringPtr = string.toNativeUtf16();
try {
final hr = WindowsCreateString(stringPtr, string.length, hString);
if (FAILED(hr)) throw WindowsException(hr);
return hString.value;
} finally {
free(stringPtr);
free(hString);
}
}
extension WindowManagerExtension on WindowManager { extension WindowManagerExtension on WindowManager {
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize(); Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
} }

View File

@@ -1,7 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:reboot_common/common.dart';
AppLocalizations? _translations; AppLocalizations? _translations;
bool _init = false; bool _init = false;
@@ -20,16 +19,3 @@ void loadTranslations(BuildContext context) {
} }
String get currentLocale => Intl.getCurrentLocale().split("_")[0]; String get currentLocale => Intl.getCurrentLocale().split("_")[0];
extension GameServerTypeExtension on GameServerType {
String get translatedName {
switch(this) {
case GameServerType.headless:
return translations.gameServerTypeHeadless;
case GameServerType.virtualWindow:
return translations.gameServerTypeVirtualWindow;
case GameServerType.window:
return translations.gameServerTypeWindow;
}
}
}

View File

@@ -1,28 +1,35 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
typedef FileSelectorValidator = String? Function(String?);
class FileSelector extends StatefulWidget { class FileSelector extends StatefulWidget {
final String placeholder; final String placeholder;
final String windowTitle; final String windowTitle;
final bool allowNavigator; final bool allowNavigator;
final TextEditingController controller; final TextEditingController controller;
final String? Function(String?) validator; final FileSelectorValidator? validator;
final AutovalidateMode? validatorMode; final AutovalidateMode? validatorMode;
final Key? validatorKey;
final String? extension; final String? extension;
final String? label; final String? label;
final bool folder; final bool folder;
final void Function(String)? onSelected;
const FileSelector( const FileSelector(
{required this.placeholder, {required this.placeholder,
required this.windowTitle, required this.windowTitle,
required this.controller, required this.controller,
required this.validator,
required this.folder, required this.folder,
required this.allowNavigator, required this.allowNavigator,
this.validator,
this.validatorKey,
this.label, this.label,
this.extension, this.extension,
this.validatorMode, this.validatorMode,
this.onSelected,
Key? key}) Key? key})
: assert(folder || extension != null, "Missing extension for file selector"), : assert(folder || extension != null, "Missing extension for file selector"),
super(key: key); super(key: key);
@@ -47,6 +54,7 @@ class _FileSelectorState extends State<FileSelector> {
placeholder: widget.placeholder, placeholder: widget.placeholder,
validator: widget.validator, validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction, autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction,
key: widget.validatorKey,
suffix: !widget.allowNavigator ? null : Button( suffix: !widget.allowNavigator ? null : Button(
onPressed: _onPressed, onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal) child: const Icon(FluentIcons.open_folder_horizontal)
@@ -72,6 +80,10 @@ class _FileSelectorState extends State<FileSelector> {
} }
void _updateText(String? value) { void _updateText(String? value) {
if(value != null) {
widget.onSelected?.call(value);
}
var text = value ?? widget.controller.text; var text = value ?? widget.controller.text;
widget.controller.text = value ?? widget.controller.text; widget.controller.text = value ?? widget.controller.text;
widget.controller.selection = TextSelection.collapsed(offset: text.length); widget.controller.selection = TextSelection.collapsed(offset: text.length);

View File

@@ -13,10 +13,14 @@ import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
const double _kButtonDimensions = 30; const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8; const double _kButtonSpacing = 8;
// FIXME: If the user clicks on the reset button, the text field checker won't be called SettingTile createFileSetting({
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) { required GlobalKey<TextFormBoxState> key,
final obx = RxString(controller.text); required String title,
controller.addListener(() => obx.value = controller.text); required String description,
required TextEditingController controller,
required void Function() onReset
}) {
final obx = RxnString();
final selecting = RxBool(false); final selecting = RxBool(false);
return SettingTile( return SettingTile(
icon: Icon( icon: Icon(
@@ -32,17 +36,22 @@ SettingTile createFileSetting({required String title, required String descriptio
placeholder: translations.selectPathPlaceholder, placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle, windowTitle: translations.selectPathWindowTitle,
controller: controller, controller: controller,
validator: _checkDll, validator: (text) {
final result = _checkDll(text);
obx.value = result;
return result;
},
extension: "dll", extension: "dll",
folder: false, folder: false,
validatorMode: AutovalidateMode.always, validatorMode: AutovalidateMode.always,
allowNavigator: false, allowNavigator: false,
validatorKey: key
), ),
), ),
const SizedBox(width: _kButtonSpacing), const SizedBox(width: _kButtonSpacing),
Obx(() => Padding( Obx(() => Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0 bottom: obx.value == null ? 0.0 : 20.0
), ),
child: Tooltip( child: Tooltip(
message: translations.selectFile, message: translations.selectFile,
@@ -63,7 +72,7 @@ SettingTile createFileSetting({required String title, required String descriptio
const SizedBox(width: _kButtonSpacing), const SizedBox(width: _kButtonSpacing),
Obx(() => Padding( Obx(() => Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0 bottom: obx.value == null ? 0.0 : 20.0
), ),
child: Tooltip( child: Tooltip(
message: translations.reset, message: translations.reset,
@@ -109,7 +118,9 @@ String? _checkDll(String? text) {
} }
final file = File(text); final file = File(text);
if (!file.existsSync()) { try {
file.readAsBytesSync();
}catch(_) {
return translations.dllDoesNotExist; return translations.dllDoesNotExist;
} }

View File

@@ -7,6 +7,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:port_forwarder/port_forwarder.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/dll_controller.dart';
@@ -14,9 +15,7 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -45,7 +44,6 @@ class _LaunchButtonState extends State<LaunchButton> {
InfoBarEntry? _gameServerInfoBar; InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation; CancelableOperation? _operation;
Completer? _pingOperation; Completer? _pingOperation;
IVirtualDesktop? _virtualDesktop;
@override @override
Widget build(BuildContext context) => Align( Widget build(BuildContext context) => Align(
@@ -73,17 +71,19 @@ class _LaunchButtonState extends State<LaunchButton> {
if (host ? _hostingController.started() : _gameController.started()) { if (host ? _hostingController.started() : _gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance"); log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop( _onStop(
reason: _StopReason.normal reason: _StopReason.normal,
host: host
); );
return; return;
} }
final version = _gameController.selectedVersion; final version = _gameController.selectedVersion.value;
log("[${host ? 'HOST' : 'GAME'}] Version data: $version"); log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
if(version == null){ if(version == null){
log("[${host ? 'HOST' : 'GAME'}] No version selected"); log("[${host ? 'HOST' : 'GAME'}] No version selected");
_onStop( _onStop(
reason: _StopReason.missingVersionError reason: _StopReason.missingVersionError,
host: host
); );
return; return;
} }
@@ -93,45 +93,47 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Set started"); log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) { for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(version.content, injectable, host) == null) { if(await _getDllFileOrStop(version.gameVersion, injectable, host) == null) {
return; return;
} }
} }
try { try {
final executable = await version.shippingExecutable;
if(executable == null){
log("[${host ? 'HOST' : 'GAME'}] No executable found");
_onStop(
reason: _StopReason.missingExecutableError,
error: version.location.path
);
return;
}
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(
reason: _StopReason.backendError reason: _StopReason.backendError,
host: host
); );
return; return;
} }
log("[${host ? 'HOST' : 'GAME'}] Backend works"); log("[${host ? 'HOST' : 'GAME'}] Backend works");
final serverType = _hostingController.type.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)"); final headless = _hostingController.headless.value;
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false); log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, false);
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance"); log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance); final result = await _startGameProcesses(version, host, headless, linkedHostingInstance);
final started = host ? _hostingController.started() : _gameController.started(); final started = host ? _hostingController.started() : _gameController.started();
if(!started) { if(!started) {
result?.kill(); result?.kill();
return; return;
} }
if(host || linkedHostingInstance != null) {
if (_dllController.gameServerPort.text == kDefaultBackendPort.toString()) {
_onStop(
reason: _StopReason.gameServerPortError,
host: host
);
return;
}
}
if(!host) { if(!host) {
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null); _showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null);
}else { }else {
_showLaunchingGameServerWidget(); _showLaunchingGameServerWidget();
} }
@@ -139,18 +141,20 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop( _onStop(
reason: _StopReason.corruptedVersionError, reason: _StopReason.corruptedVersionError,
error: exception.toString(), error: exception.toString(),
stackTrace: stackTrace stackTrace: stackTrace,
host: host
); );
} catch (exception, stackTrace) { } catch (exception, stackTrace) {
_onStop( _onStop(
reason: _StopReason.unknownError, reason: _StopReason.unknownError,
error: exception.toString(), error: exception.toString(),
stackTrace: stackTrace stackTrace: stackTrace,
host: host
); );
} }
} }
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, GameServerType hostType, bool forceLinkedHosting) async { Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool forceLinkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically..."); log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
if(host){ if(host){
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary"); log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
@@ -174,7 +178,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server..."); log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
final instance = await _startGameProcesses(version, true, hostType, null); final instance = await _startGameProcesses(version, true, headless, null);
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server..."); log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
_setStarted(true, true); _setStarted(true, true);
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started"); log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
@@ -184,7 +188,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _askForAutomaticGameServer(bool host) async { Future<bool> _askForAutomaticGameServer(bool host) async {
if (host ? !_hostingController.started() : !_gameController.started()) { if (host ? !_hostingController.started() : !_gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance"); log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop(reason: _StopReason.normal); _onStop(reason: _StopReason.normal, host: host);
return false; return false;
} }
@@ -208,19 +212,10 @@ class _LaunchButtonState extends State<LaunchButton> {
return result; return result;
} }
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameServerType hostType, GameInstance? linkedHosting) async { Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process..."); final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher..."); final eacProcess = await _createPausedProcess(version, host, kEacExe);
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable); final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
log("[${host ? 'HOST' : 'GAME'}] Started paused launcher: $launcherProcess");
log("[${host ? 'HOST' : 'GAME'}] Starting paused eac...");
final eacProcess = await _createPausedProcess(version, version.eacExecutable);
log("[${host ? 'HOST' : 'GAME'}] Started paused eac: $eacProcess");
final executable = await version.shippingExecutable;
log("[${host ? 'HOST' : 'GAME'}] Using game path: ${executable?.path}");
final gameProcess = await _createGameProcess(version, executable!, host, hostType, linkedHosting);
if(gameProcess == null) { if(gameProcess == null) {
log("[${host ? 'HOST' : 'GAME'}] No game process was created"); log("[${host ? 'HOST' : 'GAME'}] No game process was created");
return null; return null;
@@ -228,11 +223,12 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}"); log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance( final instance = GameInstance(
version: version.content, version: version.gameVersion,
host: host,
gamePid: gameProcess, gamePid: gameProcess,
launcherPid: launcherProcess, launcherPid: launcherProcess,
eacPid: eacProcess, eacPid: eacProcess,
serverType: host ? hostType : null, headless: host && headless,
child: linkedHosting child: linkedHosting
); );
if(host){ if(host){
@@ -246,22 +242,60 @@ class _LaunchButtonState extends State<LaunchButton> {
return instance; return instance;
} }
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async { Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
try {
log("[${host ? 'HOST' : 'GAME'}] Deleting $kGFSDKAftermathLibDll...");
final dlls = await findFiles(version.location, kGFSDKAftermathLibDll);
log("[${host ? 'HOST' : 'GAME'}] Found ${dlls.length} to delete for $kGFSDKAftermathLibDll");
for(final dll in dlls) {
log("[${host ? 'HOST' : 'GAME'}] Deleting ${dll.path}...");
final result = await delete(dll);
if(result) {
log("[${host ? 'HOST' : 'GAME'}] Deleted ${dll.path}");
}else {
log("[${host ? 'HOST' : 'GAME'}] Cannot delete ${dll.path}");
}
}
}catch(_) {
}
final shippingExecutables = await findFiles(version.location, kShippingExe);
if(shippingExecutables.isEmpty){
log("[${host ? 'HOST' : 'GAME'}] No game executable found");
_onStop(
reason: _StopReason.missingExecutableError,
error: kShippingExe,
host: host
);
return null;
}
if(shippingExecutables.length != 1) {
log("[${host ? 'HOST' : 'GAME'}] Too many game executables found");
_onStop(
reason: _StopReason.multipleExecutablesError,
error: kShippingExe,
host: host
);
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Generating instance args..."); log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs( final gameArgs = createRebootArgs(
host ? _hostingController.accountUsername.text : _gameController.username.text, host ? _hostingController.accountUsername.text : _gameController.username.text,
host ? _hostingController.accountPassword.text :_gameController.password.text, host ? _hostingController.accountPassword.text : _gameController.password.text,
host, host,
hostType, headless,
false, false,
host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
); );
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}"); log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
final gameProcess = await startProcess( final gameProcess = await startProcess(
executable: executable, executable: shippingExecutables.first,
args: gameArgs, args: gameArgs,
useTempBatch: false, useTempBatch: false,
name: "${version.content}-${host ? 'HOST' : 'GAME'}", name: "${version.gameVersion}-${host ? 'HOST' : 'GAME'}",
environment: { environment: {
"OPENSSL_ia32cap": "~0x20000000" "OPENSSL_ia32cap": "~0x20000000"
} }
@@ -272,26 +306,26 @@ class _LaunchButtonState extends State<LaunchButton> {
handleGameOutput( handleGameOutput(
line: line, line: line,
host: host, host: host,
onShutdown: () => _onStop(reason: _StopReason.normal), onShutdown: () => _onStop(reason: _StopReason.normal, host: host),
onTokenError: () => _onStop(reason: _StopReason.tokenError), onTokenError: () => _onStop(reason: _StopReason.tokenError, host: host),
onBuildCorrupted: () { onBuildCorrupted: () {
if(instance == null) { if(instance == null) {
return; return;
}else if(!instance.launched) { }else if(!instance.launched) {
_onStop(reason: _StopReason.corruptedVersionError); _onStop(reason: _StopReason.corruptedVersionError, host: host);
}else { }else {
_onStop(reason: _StopReason.crash); _onStop(reason: _StopReason.crash, host: host);
} }
}, },
onLoggedIn: () =>_onLoggedIn(host), onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version), onMatchEnd: () => _onMatchEnd(version)
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
); );
} }
gameProcess.stdOutput.listen((line) => onGameOutput(line, false)); gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
gameProcess.stdError.listen((line) => onGameOutput(line, true)); gameProcess.stdError.listen((line) => onGameOutput(line, true));
gameProcess.exitCode.then((_) async { gameProcess.exitCode.then((_) async {
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
instance?.killed = true;
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal"); log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
_onStop( _onStop(
reason: _StopReason.exitCode, reason: _StopReason.exitCode,
@@ -301,60 +335,37 @@ class _LaunchButtonState extends State<LaunchButton> {
return gameProcess.pid; return gameProcess.pid;
} }
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async { Future<int?> _createPausedProcess(FortniteVersion version, bool host, String executableName) async {
if (file == null) { log("[${host ? 'HOST' : 'GAME'}] Starting $executableName...");
final executables = await findFiles(version.location, executableName);
if(executables.isEmpty){
return null;
}
if(executables.length != 1) {
log("[${host ? 'HOST' : 'GAME'}] Too many $executableName found: $executables");
_onStop(
reason: _StopReason.multipleExecutablesError,
error: executableName,
host: host
);
return null; return null;
} }
final process = await startProcess( final process = await startProcess(
executable: file, executable: executables.first,
useTempBatch: false, useTempBatch: false,
name: "${version.content}-${basenameWithoutExtension(file.path)}", name: "${version.gameVersion}-${basenameWithoutExtension(executables.first.path)}",
environment: { environment: {
"OPENSSL_ia32cap": "~0x20000000" "OPENSSL_ia32cap": "~0x20000000"
} }
); );
log("[${host ? 'HOST' : 'GAME'}] Started paused $executableName: $process");
final pid = process.pid; final pid = process.pid;
suspend(pid); suspend(pid);
return pid; return pid;
} }
Future<void> _onDisplayAttached(bool host, GameServerType type, FortniteVersion version) async {
if(host && type == GameServerType.virtualWindow) {
final hostingInstance = _hostingController.instance.value;
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
hostingInstance.movedToVirtualDesktop = true;
try {
final windowManager = VirtualDesktopManager.getInstance();
_virtualDesktop = windowManager.createDesktop();
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
var success = false;
try {
success = await windowManager.moveWindowToDesktop(
hostingInstance.gamePid,
_virtualDesktop!,
excludedWindowName: "Reboot"
);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
success = false;
}
if(!success) {
try {
windowManager.removeDesktop(_virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}finally {
_virtualDesktop = null;
}
}
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}
}
}
}
void _onMatchEnd(FortniteVersion version) { void _onMatchEnd(FortniteVersion version) {
if(_hostingController.autoRestart.value) { if(_hostingController.autoRestart.value) {
final notification = LocalNotification( final notification = LocalNotification(
@@ -397,7 +408,9 @@ class _LaunchButtonState extends State<LaunchButton> {
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
instance.launched = true; instance.launched = true;
instance.tokenError = false; instance.tokenError = false;
if(_isChapterOne(instance.version)) {
await _injectOrShowError(InjectableDll.memoryLeak, host); await _injectOrShowError(InjectableDll.memoryLeak, host);
}
if(!host){ if(!host){
await _injectOrShowError(InjectableDll.console, host); await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected(); _onGameClientInjected();
@@ -412,6 +425,14 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
bool _isChapterOne(String version) {
try {
return Version.parse(version).major < 10;
} on FormatException catch(_) {
return true;
}
}
void _onGameClientInjected() { void _onGameClientInjected() {
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
showRebootInfoBar( showRebootInfoBar(
@@ -428,36 +449,30 @@ class _LaunchButtonState extends State<LaunchButton> {
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
} }
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
try { try {
_gameServerInfoBar = showRebootInfoBar(
translations.waitingForGameServer,
loading: true,
duration: null
);
final gameServerPort = _dllController.gameServerPort.text; final gameServerPort = _dllController.gameServerPort.text;
final pingOperation = pingGameServerOrTimeout( final started = await _checkLocalGameServer(gameServerPort);
"127.0.0.1:$gameServerPort", if(!started) {
const Duration(minutes: 2) if (_hostingController.instance.value?.killed != true) {
);
this._pingOperation = pingOperation;
final localPingResult = await pingOperation.future;
_gameServerInfoBar?.close();
if (!localPingResult) {
showRebootInfoBar( showRebootInfoBar(
translations.gameServerStartWarning, translations.gameServerStartWarning,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
}
return; return;
} }
_backendController.joinLocalhost(); _backendController.joinLocalhost();
final accessible = await _checkGameServer(theme, gameServerPort); final accessible = await _checkPublicGameServer(gameServerPort);
if (!accessible) { if (!accessible) {
showRebootInfoBar( showRebootInfoBar(
translations.gameServerStartLocalWarning, translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning, severity: InfoBarSeverity.warning,
duration: infoBarLongDuration duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
child: Text(translations.checkGameServerFixAction),
),
); );
return; return;
} }
@@ -476,7 +491,30 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async { Future<bool> _checkLocalGameServer(String gameServerPort) async {
try {
_gameServerInfoBar = showRebootInfoBar(
translations.waitingForGameServer,
loading: true,
duration: null
);
final gameServerPort = _dllController.gameServerPort.text;
final pingOperation = pingGameServerOrTimeout(
"127.0.0.1:$gameServerPort",
const Duration(minutes: 2)
);
this._pingOperation = pingOperation;
final localPingResult = await pingOperation.future;
_gameServerInfoBar?.close();
return localPingResult;
}catch(_) {
_gameServerInfoBar?.close();
return false;
}
}
Future<bool> _checkPublicGameServer(String gameServerPort) async {
try { try {
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = showRebootInfoBar(
translations.checkingGameServer, translations.checkingGameServer,
@@ -484,50 +522,72 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
final publicIp = await Ipify.ipv4(); final publicIp = await Ipify.ipv4();
final available = await pingGameServer("$publicIp:$gameServerPort"); var pingOperation = await pingGameServerOrTimeout(
if(available) { "$publicIp:$gameServerPort",
const Duration(seconds: 10)
);
_pingOperation = pingOperation;
var publicPingResult = await pingOperation.future;
if (publicPingResult) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return true; return true;
} }
final pingOperation = pingGameServerOrTimeout( final gateway = await Gateway.discover();
if (gateway == null) {
_gameServerInfoBar?.close();
return false;
}
final forwarded = await gateway.openPort(
protocol: PortType.udp,
externalPort: int.parse(gameServerPort),
portDescription: "Reboot Game Server"
);
if (!forwarded) {
_gameServerInfoBar?.close();
return false;
}
// Give the modem a couple of seconds just in case
// This is not technically necessary, but I can't guarantee that the modem has no race conditions
// So might as well wait
await Future.delayed(const Duration(seconds: 5));
pingOperation = await pingGameServerOrTimeout(
"$publicIp:$gameServerPort", "$publicIp:$gameServerPort",
const Duration(days: 1) const Duration(seconds: 10)
); );
this._pingOperation = pingOperation; _pingOperation = pingOperation;
_gameServerInfoBar = showRebootInfoBar( publicPingResult = await pingOperation.future;
translations.checkGameServerFixMessage(gameServerPort),
action: Button(
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
child: Text(translations.checkGameServerFixAction),
),
severity: InfoBarSeverity.warning,
duration: null,
loading: true
);
final result = await pingOperation.future;
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return result; return publicPingResult;
}finally { }catch(_) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return false;
} }
} }
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async { Future<void> _onStop({
if(host == null) { required _StopReason reason,
required bool host,
String? error,
StackTrace? stackTrace,
bool interactive = true
}) async {
if(host) {
try { try {
_pingOperation?.complete(false); _pingOperation?.complete(false);
}catch(_) { } catch (_) {
// Ignore: might be running, don't bother checking // Ignore: might have been already terminated, don't bother checking
} finally { } finally {
_pingOperation = null; _pingOperation = null;
} }
await _operation?.cancel();
_operation = null;
_backendController.stop();
} }
host = host ?? widget.host; await _operation?.cancel();
_operation = null;
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){ if(host){
@@ -536,15 +596,6 @@ class _LaunchButtonState extends State<LaunchButton> {
_gameController.instance.value = null; _gameController.instance.value = null;
} }
if(_virtualDesktop != null) {
try {
final instance = VirtualDesktopManager.getInstance();
instance.removeDesktop(_virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] Cannot close virtual desktop: $error");
}
}
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace"); log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}"); log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
if(host) { if(host) {
@@ -560,11 +611,16 @@ class _LaunchButtonState extends State<LaunchButton> {
if(child != null) { if(child != null) {
await _onStop( await _onStop(
reason: reason, reason: reason,
host: child.serverType != null host: child.host,
error: error,
stackTrace: stackTrace,
interactive: false
); );
} }
_setStarted(host, false); _setStarted(host, false);
if(interactive) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) { if(host == true) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
@@ -592,18 +648,27 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.multipleExecutablesError:
showRebootInfoBar(
translations.multipleExecutablesError(error ?? translations.unknown),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.exitCode: case _StopReason.exitCode:
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
final injectedDlls = instance.injectedDlls;
showRebootInfoBar( showRebootInfoBar(
translations.corruptedVersionError, translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
} }
break; break;
case _StopReason.corruptedVersionError: case _StopReason.corruptedVersionError:
final injectedDlls = instance?.injectedDlls ?? [];
showRebootInfoBar( showRebootInfoBar(
translations.corruptedVersionError, translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button( action: Button(
@@ -627,9 +692,10 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.tokenError: case _StopReason.tokenError:
_backendController.stop(); _backendController.stop(interactive: false);
final injectedDlls = instance?.injectedDlls;
showRebootInfoBar( showRebootInfoBar(
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")), translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button( action: Button(
@@ -640,7 +706,7 @@ class _LaunchButtonState extends State<LaunchButton> {
break; break;
case _StopReason.crash: case _StopReason.crash:
showRebootInfoBar( showRebootInfoBar(
translations.fortniteCrashError(host ? "game server" : "client"), translations.fortniteCrashError(host ? translations.gameServer : translations.client),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
@@ -652,6 +718,14 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.gameServerPortError:
showRebootInfoBar(
translations.gameServerPortEqualsBackendPort(kDefaultBackendPort),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
}
} }
} }
@@ -667,17 +741,13 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess"); log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting); final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath"); log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
if(dllPath == null) { if (dllPath == null) {
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist"); log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: hosting
);
return; return;
} }
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}..."); log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable
.name}...");
await injectDll(gameProcess, dllPath); await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable); instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}"); log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
@@ -692,13 +762,16 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async { Future<File?> _getDllFileOrStop(String version, InjectableDll injectable, bool host) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}..."); log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final (file, customDll) = _dllController.getInjectableData(version, injectable); final (file, customDll) = _dllController.getInjectableData(version, injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll"); log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) { try {
await file.readAsBytes();
log("[${host ? 'HOST' : 'GAME'}] Path exists"); log("[${host ? 'HOST' : 'GAME'}] Path exists");
return file; return file;
}catch(_) {
} }
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist"); log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
@@ -707,14 +780,20 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop( _onStop(
reason: _StopReason.missingCustomDllError, reason: _StopReason.missingCustomDllError,
error: injectable.name, error: injectable.name,
host: host
); );
return null; return null;
} }
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again..."); log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _dllController.download(injectable, file.path, force: true); final result = await _dllController.download(injectable, file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); if(result) {
return _getDllFileOrStop(version, injectable, host, true); log("[${host ? 'HOST' : 'GAME'}] Downloaded critical dll");
return file;
}
_onStop(reason: _StopReason.normal, host: host);
return null;
} }
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar( InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
@@ -723,7 +802,7 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) { InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar( return _gameClientInfoBar = showRebootInfoBar(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly, linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true, loading: true,
@@ -741,9 +820,9 @@ class _LaunchButtonState extends State<LaunchButton> {
onPressed: () async { onPressed: () async {
_backendController.joinLocalhost(); _backendController.joinLocalhost();
if(!_hostingController.started.value) { if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true); _gameController.instance.value?.child = await _startMatchMakingServer(version, false, headless, true);
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
_showLaunchingGameClientWidget(version, hostType, true); _showLaunchingGameClientWidget(version, headless, true);
} }
}, },
child: Text(translations.startGameServer), child: Text(translations.startGameServer),
@@ -758,6 +837,7 @@ enum _StopReason {
normal, normal,
missingVersionError, missingVersionError,
missingExecutableError, missingExecutableError,
multipleExecutablesError,
corruptedVersionError, corruptedVersionError,
missingCustomDllError, missingCustomDllError,
corruptedDllError, corruptedDllError,
@@ -765,6 +845,7 @@ enum _StopReason {
matchmakerError, matchmakerError,
tokenError, tokenError,
unknownError, unknownError,
gameServerPortError,
exitCode, exitCode,
crash; crash;

View File

@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog( Future<void> showDllDeletedDialog() => showRebootDialog(
builder: (context) => InfoDialog( builder: (context) => InfoDialog(
text: translations.dllDeletedTitle, text: translations.dllDeletedTitle,
buttons: [ buttons: [
@@ -15,7 +15,7 @@ Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
text: translations.dllDeletedPrimaryAction, text: translations.dllDeletedPrimaryAction,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
onConfirm();
}, },
), ),
], ],

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

@@ -180,7 +180,7 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
case _Filter.accessible: case _Filter.accessible:
return element.password == null; return element.password == null;
case _Filter.playable: case _Filter.playable:
return _gameController.getVersionByName(element.version) != null; return _gameController.getVersionByGame(element.version) != null;
} }
}).toList(); }).toList();
final sort = _sort.value; final sort = _sort.value;

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);
}); });
} }
@@ -133,7 +134,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
dllsDirectory.createSync(recursive: true); dllsDirectory.createSync(recursive: true);
} }
_dllController.guardFiles(); _dllController.downloadAndGuardDependencies();
} }
@override @override
@@ -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

@@ -5,12 +5,10 @@ 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/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart'; import 'package:reboot_launcher/src/widget/message/data.dart';
@@ -19,7 +17,7 @@ import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart'; import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart'; import 'package:reboot_launcher/src/widget/version/version_selector.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey(); final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey(); final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
@@ -78,7 +76,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
@override @override
List<Widget> get settings => [ List<Widget> get settings => [
_information, _information,
buildVersionSelector( VersionSelector.buildTile(
key: hostVersionOverlayTargetKey key: hostVersionOverlayTargetKey
), ),
_options, _options,
@@ -133,7 +131,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
FluentIcons.password_24_regular FluentIcons.password_24_regular
), ),
title: Text(translations.hostGameServerPasswordName), title: Text(translations.hostGameServerPasswordName),
subtitle: Text(translations.hostGameServerDescriptionDescription), subtitle: Text(translations.hostGameServerPasswordDescription),
content: Obx(() => OverlayTarget( content: Obx(() => OverlayTarget(
key: hostInfoPasswordOverlayTargetKey, key: hostInfoPasswordOverlayTargetKey,
child: TextFormBox( child: TextFormBox(
@@ -214,15 +212,21 @@ class _HostingPageState extends RebootPageState<HostPage> {
), ),
title: Text(translations.gameServerTypeName), title: Text(translations.gameServerTypeName),
subtitle: Text(translations.gameServerTypeDescription), subtitle: Text(translations.gameServerTypeDescription),
content: Obx(() => DropDownButton( contentWidth: null,
onOpen: () => inDialog = true, content: Row(
onClose: () => inDialog = false, children: [
leading: Text(_hostingController.type.value.translatedName), Obx(() => Text(
items: GameServerType.values.map((entry) => MenuFlyoutItem( _hostingController.headless.value ? translations.on : translations.off
text: Text(entry.translatedName),
onPressed: () => _hostingController.type.value = entry
)).toList()
)), )),
const SizedBox(
width: 16.0
),
Obx(() => ToggleSwitch(
checked: _hostingController.headless.value,
onChanged: (value) => _hostingController.headless.value = value
)),
],
),
), ),
SettingTile( SettingTile(
icon: Icon( icon: Icon(

View File

@@ -10,7 +10,7 @@ import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart'; import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart'; import 'package:reboot_launcher/src/widget/version/version_selector.dart';
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey(); final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
@@ -46,7 +46,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
@override @override
List<SettingTile> get settings => [ List<SettingTile> get settings => [
buildVersionSelector( VersionSelector.buildTile(
key: gameVersionOverlayTargetKey key: gameVersionOverlayTargetKey
), ),
_options, _options,

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';
@@ -14,6 +17,11 @@ import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart'; import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsAuthDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsMemoryDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsGameServerDllInputKey = GlobalKey();
class SettingsPage extends RebootPage { class SettingsPage extends RebootPage {
const SettingsPage({Key? key}) : super(key: key); const SettingsPage({Key? key}) : super(key: key);
@@ -36,6 +44,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;
@@ -56,33 +65,39 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
subtitle: Text(translations.settingsClientDescription), subtitle: Text(translations.settingsClientDescription),
children: [ children: [
createFileSetting( createFileSetting(
key: settingsConsoleDllInputKey,
title: translations.settingsClientConsoleName, title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription, description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll, controller: _dllController.unrealEngineConsoleDll,
onReset: () { onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.console); final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path; _dllController.unrealEngineConsoleDll.text = path;
_dllController.download(InjectableDll.console, path, force: true); await _dllController.download(InjectableDll.console, path, force: true);
settingsConsoleDllInputKey.currentState?.validate();
} }
), ),
createFileSetting( createFileSetting(
key: settingsAuthDllInputKey,
title: translations.settingsClientAuthName, title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription, description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll, controller: _dllController.backendDll,
onReset: () { onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.auth); final path = _dllController.getDefaultDllPath(InjectableDll.auth);
_dllController.backendDll.text = path; _dllController.backendDll.text = path;
_dllController.download(InjectableDll.auth, path, force: true); await _dllController.download(InjectableDll.auth, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
} }
), ),
createFileSetting( createFileSetting(
key: settingsMemoryDllInputKey,
title: translations.settingsClientMemoryName, title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription, description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll, controller: _dllController.memoryLeakDll,
onReset: () { onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak); final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
_dllController.memoryLeakDll.text = path; _dllController.memoryLeakDll.text = path;
_dllController.download(InjectableDll.memoryLeak, path, force: true); await _dllController.download(InjectableDll.memoryLeak, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
} }
), ),
_internalFilesServerType, _internalFilesServerType,
@@ -115,7 +130,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 +155,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),
@@ -182,18 +192,38 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
); );
}else { }else {
return createFileSetting( return createFileSetting(
key: settingsGameServerDllInputKey,
title: translations.settingsOldServerFileName, title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription, description: translations.settingsServerFileDescription,
controller: _dllController.gameServerDll, controller: _dllController.customGameServerDll,
onReset: () { onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer); final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
_dllController.gameServerDll.text = path; _dllController.customGameServerDll.text = path;
_dllController.download(InjectableDll.gameServer, path); await _dllController.download(InjectableDll.gameServer, path);
settingsGameServerDllInputKey.currentState?.validate();
} }
); );
} }
}); });
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 +239,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 +299,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

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
@@ -12,46 +11,42 @@ import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/util/types.dart'; import 'package:reboot_launcher/src/util/types.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart'; import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:windows_taskbar/windows_taskbar.dart'; import 'package:windows_taskbar/windows_taskbar.dart';
class AddVersionDialog extends StatefulWidget { class DownloadVersionDialog extends StatefulWidget {
final bool closable; final bool closable;
const AddVersionDialog({Key? key, required this.closable}) : super(key: key); const DownloadVersionDialog({Key? key, required this.closable}) : super(key: key);
@override @override
State<AddVersionDialog> createState() => _AddVersionDialogState(); State<DownloadVersionDialog> createState() => _DownloadVersionDialogState();
} }
class _AddVersionDialogState extends State<AddVersionDialog> { class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _pathController = TextEditingController(); final TextEditingController _pathController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey(); final GlobalKey<FormState> _formKey = GlobalKey();
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey(); final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form); final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
final Rx<_BuildSource> _source = Rx(_BuildSource.githubArchive);
final Rxn<FortniteBuild> _build = Rxn(); final Rxn<FortniteBuild> _build = Rxn();
final RxnInt _timeLeft = RxnInt(); final RxnInt _timeLeft = RxnInt();
final Rxn<double> _progress = Rxn(); final Rxn<double> _progress = Rxn();
final RxInt _speed = RxInt(0); final RxInt _speed = RxInt(0);
late Future<List<FortniteBuild>> _fetchFuture;
SendPort? _downloadPort; SendPort? _downloadPort;
Object? _error; Object? _error;
StackTrace? _stackTrace; StackTrace? _stackTrace;
@override @override
void initState() { void initState() {
_fetchFuture = compute(fetchBuilds, null).then((value) {
_updateFormDefaults();
return value;
});
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
_nameController.dispose();
_pathController.dispose(); _pathController.dispose();
_cancelDownload(); _cancelDownload();
super.dispose(); super.dispose();
@@ -60,6 +55,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
void _cancelDownload() { void _cancelDownload() {
_downloadPort?.send(kStopBuildDownloadSignal); _downloadPort?.send(kStopBuildDownloadSignal);
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
stopDownloadServer();
} }
@override @override
@@ -68,27 +64,9 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
child: Obx(() { child: Obx(() {
switch(_status.value){ switch(_status.value){
case _DownloadStatus.form: case _DownloadStatus.form:
return FutureBuilder( return FormDialog(
future: _fetchFuture, content: _formBody,
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 buttons: _formButtons
));
}
); );
case _DownloadStatus.downloading: case _DownloadStatus.downloading:
case _DownloadStatus.extracting: case _DownloadStatus.extracting:
@@ -97,15 +75,24 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
buttons: _stopButton buttons: _stopButton
); );
case _DownloadStatus.error: case _DownloadStatus.error:
return ErrorDialog( final build = _build.value;
exception: _error ?? Exception(translations.unknownError), var error = _error?.toString() ?? translations.unknownError;
stackTrace: _stackTrace,
errorMessageBuilder: (exception) {
var error = exception.toString();
error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error; error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error;
error = error.toLowerCase(); error = error.toLowerCase();
return translations.downloadVersionError(error); return InfoDialog(
} text: translations.downloadVersionError(error),
buttons: [
DialogButton(
type: ButtonType.secondary,
text: translations.defaultDialogSecondaryAction
),
if(build != null)
DialogButton(
type: ButtonType.primary,
text: translations.downloadManually,
onTap: () => launchUrlString(build.link)
),
],
); );
case _DownloadStatus.done: case _DownloadStatus.done:
return InfoDialog( return InfoDialog(
@@ -119,7 +106,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
if(widget.closable) if(widget.closable)
DialogButton(type: ButtonType.secondary), DialogButton(type: ButtonType.secondary),
DialogButton( DialogButton(
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download, text: translations.download,
type: widget.closable ? ButtonType.primary : ButtonType.only, type: widget.closable ? ButtonType.primary : ButtonType.only,
color: FluentTheme.of(context).accentColor, color: FluentTheme.of(context).accentColor,
onTap: () => _startDownload(context), onTap: () => _startDownload(context),
@@ -143,13 +130,6 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return; return;
} }
final source = _source.value;
if(source == _BuildSource.local) {
Navigator.of(context).pop();
_addFortniteVersion(build);
return;
}
_status.value = _DownloadStatus.downloading; _status.value = _DownloadStatus.downloading;
final communicationPort = ReceivePort(); final communicationPort = ReceivePort();
communicationPort.listen((message) { communicationPort.listen((message) {
@@ -184,16 +164,22 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return; return;
} }
_status.value = _DownloadStatus.done; final name = _nameController.text.trim();
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); final location = Directory(_pathController.text);
_addFortniteVersion(build); final files = await findFiles(location, kShippingExe);
if(files.length == 1) {
await patchHeadless(files.first);
} }
void _addFortniteVersion(FortniteBuild build) { _status.value = _DownloadStatus.done;
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion( WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
content: build.version,
location: Directory(_pathController.text) final version = FortniteVersion(
))); name: name,
gameVersion: build.gameVersion,
location: location
);
_gameController.addVersion(version);
} }
void _onDownloadError(Object? error, StackTrace? stackTrace) { void _onDownloadError(Object? error, StackTrace? stackTrace) {
@@ -256,7 +242,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
style: FluentTheme.maybeOf(context)?.typography.body, style: FluentTheme.maybeOf(context)?.typography.body,
), ),
if(timeLeft != null) if(timeLeft != null && timeLeft != -1)
Text( Text(
translations.timeLeft(timeLeft), translations.timeLeft(timeLeft),
style: FluentTheme.maybeOf(context)?.typography.body, style: FluentTheme.maybeOf(context)?.typography.body,
@@ -292,25 +278,32 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return translations.downloading; return translations.downloading;
} }
Widget _buildFormBody(List<FortniteBuild> builds) { Widget get _formBody => Column(
return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildSourceSelector(), InfoLabel(
label: translations.versionName,
child: TextFormBox(
controller: _nameController,
validator: _checkVersionName,
placeholder: translations.versionNameLabel,
autovalidateMode: AutovalidateMode.onUserInteraction
),
),
const SizedBox( const SizedBox(
height: 16.0 height: 16.0
), ),
_buildBuildSelector(builds), _buildSelector,
FileSelector( FileSelector(
label: translations.gameFolderTitle, label: translations.gameFolderTitle,
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder, placeholder: translations.buildInstallationDirectoryPlaceholder,
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle, windowTitle: translations.buildInstallationDirectoryWindowTitle,
controller: _pathController, controller: _pathController,
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination, validator: _checkDownloadDestination,
folder: true, folder: true,
allowNavigator: true allowNavigator: true
), ),
@@ -320,20 +313,14 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
) )
], ],
); );
}
String? _checkGameFolder(text) { String? _checkVersionName(text) {
if (text == null || text.isEmpty) { if (text == null || text.isEmpty) {
return translations.emptyGamePath; return translations.emptyVersionName;
} }
final directory = Directory(text); if(_gameController.getVersionByName(text) != null) {
if (!directory.existsSync()) { return translations.versionAlreadyExists;
return translations.directoryDoesNotExist;
}
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
return translations.missingShippingExe;
} }
return null; return null;
@@ -347,7 +334,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return null; return null;
} }
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel( Widget get _buildSelector => InfoLabel(
label: translations.build, label: translations.build,
child: FormField<FortniteBuild?>( child: FormField<FortniteBuild?>(
key: _formFieldKey, key: _formFieldKey,
@@ -358,7 +345,9 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
ComboBox<FortniteBuild>( ComboBox<FortniteBuild>(
placeholder: Text(translations.selectBuild), placeholder: Text(translations.selectBuild),
isExpanded: true, isExpanded: true,
items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(), items: downloadableBuilds.where((build) => build.available)
.map((element) => _buildBuildItem(element))
.toList(),
value: _build.value, value: _build.value,
onChanged: (value) { onChanged: (value) {
if(value == null){ if(value == null){
@@ -393,40 +382,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return translations.selectBuild; return translations.selectBuild;
} }
final versions = _gameController.versions.value;
if (versions.any((element) => data.version == element.content)) {
return translations.versionAlreadyExists;
}
return null; return null;
} }
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>( ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
value: element, value: element,
child: Text(element.version.toString()) child: Text(element.gameVersion)
);
Widget _buildSourceSelector() => InfoLabel(
label: translations.source,
child: ComboBox<_BuildSource>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: _BuildSource.values.map((entry) => _buildSourceItem(entry)).toList(),
value: _source.value,
onChanged: (value) {
if(value == null){
return;
}
_source.value = value;
_updateFormDefaults();
}
)
);
ComboBoxItem<_BuildSource> _buildSourceItem(_BuildSource element) => ComboBoxItem<_BuildSource>(
value: element,
child: Text(element.translatedName)
); );
@@ -438,23 +399,22 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
]; ];
Future<void> _updateFormDefaults() async { Future<void> _updateFormDefaults() async {
if(_source.value != _BuildSource.local && _build.value?.available != true) { if(_build.value?.available != true) {
_build.value = null; _build.value = null;
} }
final disks = WindowsDisk.available();
if(_source.value != _BuildSource.local && disks.isNotEmpty) {
final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
final build = _build.value; final build = _build.value;
if(build == null){ if(build != null) {
return; _nameController.text = build.gameVersion;
} _nameController.selection = TextSelection.collapsed(offset: build.gameVersion.length);
final disks = WindowsDisk.available();
print("${bestDisk.path}\\FortniteBuilds\\${build.version}"); if(disks.isNotEmpty) {
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}"; final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
final pathText = "${bestDisk.path}FortniteBuilds\\${build.gameVersion}";
_pathController.text = pathText; _pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length); _pathController.selection = TextSelection.collapsed(offset: pathText.length);
} }
}
_formKey.currentState?.validate(); _formKey.currentState?.validate();
} }
@@ -467,18 +427,3 @@ enum _DownloadStatus {
error, error,
done done
} }
enum _BuildSource {
local,
githubArchive;
String get translatedName {
switch(this) {
case _BuildSource.local:
return translations.localBuild;
case _BuildSource.githubArchive:
return translations.githubArchive;
}
}
}

View File

@@ -0,0 +1,231 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:path/path.dart' as path;
import 'package:version/version.dart';
class ImportVersionDialog extends StatefulWidget {
final FortniteVersion? version;
final bool closable;
const ImportVersionDialog({Key? key, required this.version, required this.closable}) : super(key: key);
@override
State<ImportVersionDialog> createState() => _ImportVersionDialogState();
}
class _ImportVersionDialogState extends State<ImportVersionDialog> {
final TextEditingController _nameController = TextEditingController();
final GameController _gameController = Get.find<GameController>();
final TextEditingController _pathController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey();
final Rx<_ImportState> _validator = Rx(_ImportState.inputData);
@override
void initState() {
final version = widget.version;
if(version != null) {
_nameController.text = version.name;
_nameController.selection = TextSelection.collapsed(offset: version.name.length);
_pathController.text = version.location.path;
_pathController.selection = TextSelection.collapsed(offset: version.location.path.length);
}
super.initState();
}
@override
void dispose() {
_nameController.dispose();
_pathController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Form(
key: _formKey,
child: Obx(() {
switch(_validator.value) {
case _ImportState.inputData:
return FormDialog(
content: _importBody,
buttons: _importButtons
);
case _ImportState.validating:
return ProgressDialog(
text: translations.importingVersion
);
case _ImportState.success:
return InfoDialog(
text: translations.importedVersion
);
case _ImportState.missingShippingExeError:
return InfoDialog(
text: translations.importVersionMissingShippingExeError(kShippingExe)
);
case _ImportState.multipleShippingExesError:
return InfoDialog(
text: translations.importVersionMultipleShippingExesError(kShippingExe)
);
case _ImportState.unsupportedVersionError:
return InfoDialog(
text: translations.importVersionUnsupportedVersionError
);
}
})
);
Widget get _importBody => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.versionName,
child: TextFormBox(
controller: _nameController,
validator: _checkVersionName,
placeholder: translations.versionNameLabel,
autovalidateMode: AutovalidateMode.onUserInteraction
),
),
const SizedBox(
height: 16.0
),
FileSelector(
label: translations.gameFolderTitle,
placeholder: translations.gameFolderPlaceholder,
windowTitle: translations.gameFolderPlaceWindowTitle,
controller: _pathController,
validator: _checkGamePath,
validatorMode: AutovalidateMode.onUserInteraction,
folder: true,
allowNavigator: true,
onSelected: (selected) {
var name = path.basename(selected);
if(_gameController.getVersionByName(name) != null) {
var counter = 1;
while(_gameController.getVersionByName("$name-$counter") != null) {
counter++;
}
name = "$name-$counter";
}
_nameController.text = name;
_nameController.selection = TextSelection.collapsed(offset: name.length);
},
),
const SizedBox(
height: 16.0
)
],
);
List<DialogButton> get _importButtons => [
if(widget.closable)
DialogButton(type: ButtonType.secondary),
DialogButton(
text: translations.saveLocalVersion,
type: widget.closable ? ButtonType.primary : ButtonType.only,
color: FluentTheme.of(context).accentColor,
onTap: _importVersion,
)
];
void _importVersion() async {
final topResult = _formKey.currentState?.validate();
if(topResult != true) {
return;
}
_validator.value = _ImportState.validating;
final name = _nameController.text.trim();
final directory = Directory(_pathController.text.trim());
final shippingExes = await Future.wait([
Future.delayed(const Duration(seconds: 1)).then((_) => <File>[]),
findFiles(directory, kShippingExe)
]).then((values) => values.expand((entry) => entry).toList());
if (shippingExes.isEmpty) {
_validator.value = _ImportState.missingShippingExeError;
return;
}
if(shippingExes.length != 1) {
_validator.value = _ImportState.multipleShippingExesError;
return;
}
await patchHeadless(shippingExes.first);
final gameVersion = await extractGameVersion(directory);
try {
if(Version.parse(gameVersion) >= kMaxAllowedVersion) {
_validator.value = _ImportState.unsupportedVersionError;
return;
}
}catch(_) {
}
if(widget.version == null) {
final version = FortniteVersion(
name: name,
gameVersion: gameVersion,
location: shippingExes.first.parent
);
_gameController.addVersion(version);
}else {
widget.version?.name = name;
widget.version?.gameVersion = gameVersion;
widget.version?.location = shippingExes.first.parent;
}
_validator.value = _ImportState.success;
}
String? _checkVersionName(String? text) {
final version = widget.version;
if(version != null && version.name == text) {
return null;
}
if (text == null || text.isEmpty) {
return translations.emptyVersionName;
}
if(_gameController.getVersionByName(text) != null) {
return translations.versionAlreadyExists;
}
return null;
}
String? _checkGamePath(String? input) {
if(input == null || input.isEmpty) {
return translations.emptyGamePath;
}
final directory = Directory(input);
if(!directory.existsSync()) {
return translations.directoryDoesNotExist;
}
return null;
}
}
enum _ImportState {
inputData,
validating,
success,
missingShippingExeError,
multipleShippingExesError,
unsupportedVersionError
}

View File

@@ -8,15 +8,46 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/widget/message/version.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/download_version.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version/import_version.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget { class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key); const VersionSelector({Key? key}) : super(key: key);
static SettingTile buildTile({
required GlobalKey<OverlayTargetState> key
}) => SettingTile(
icon: Icon(
FluentIcons.play_24_regular
),
title: Text(translations.selectFortniteName),
subtitle: Text(translations.selectFortniteDescription),
contentWidth: null,
content: ConstrainedBox(
constraints: BoxConstraints(
minWidth: SettingTile.kDefaultContentWidth,
),
child: OverlayTarget(
key: key,
child: const VersionSelector(),
)
)
);
static Future<void> openImportDialog(FortniteVersion? version) => showRebootDialog<bool>(
builder: (context) => ImportVersionDialog(
version: version,
closable: true,
),
dismissWithEsc: true
);
static Future<void> openDownloadDialog() => showRebootDialog<bool>( static Future<void> openDownloadDialog() => showRebootDialog<bool>(
builder: (context) => AddVersionDialog( builder: (context) => DownloadVersionDialog(
closable: true, closable: true,
), ),
dismissWithEsc: true dismissWithEsc: true
@@ -34,7 +65,7 @@ class _VersionSelectorState extends State<VersionSelector> {
@override @override
Widget build(BuildContext context) => Obx(() { Widget build(BuildContext context) => Obx(() {
return _createOptionsMenu( return _createOptionsMenu(
version: _gameController.selectedVersion, version: _gameController.selectedVersion.value,
close: false, close: false,
child: FlyoutTarget( child: FlyoutTarget(
controller: _flyoutController, controller: _flyoutController,
@@ -42,7 +73,7 @@ class _VersionSelectorState extends State<VersionSelector> {
onOpen: () => inDialog = true, onOpen: () => inDialog = true,
onClose: () => inDialog = false, onClose: () => inDialog = false,
leading: Text( leading: Text(
_gameController.selectedVersion?.content.toString() ?? translations.selectVersion, _gameController.selectedVersion.value?.name ?? translations.selectVersion,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -65,7 +96,7 @@ class _VersionSelectorState extends State<VersionSelector> {
var result = await _flyoutController.showFlyout<_ContextualOption?>( var result = await _flyoutController.showFlyout<_ContextualOption?>(
builder: (context) => MenuFlyout( builder: (context) => MenuFlyout(
items: _ContextualOption.values items: _ContextualOption.values
.map((entry) => _createOption(context, entry)) .map((entry) => _createOption(entry))
.toList() .toList()
) )
); );
@@ -76,7 +107,7 @@ class _VersionSelectorState extends State<VersionSelector> {
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) { List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
final items = _gameController.versions.value final items = _gameController.versions.value
.map((version) => _createVersionItem(context, version)) .map((version) => _createVersionItem(version))
.toList(); .toList();
items.add(MenuFlyoutItem( items.add(MenuFlyoutItem(
trailing: Padding( trailing: Padding(
@@ -87,12 +118,23 @@ class _VersionSelectorState extends State<VersionSelector> {
), ),
), ),
text: Text(translations.addVersion), text: Text(translations.addVersion),
onPressed: () => VersionSelector.openImportDialog(null)
));
items.add(MenuFlyoutItem(
trailing: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
FluentIcons.arrow_download_24_regular,
size: 14
),
),
text: Text(translations.downloadVersion),
onPressed: VersionSelector.openDownloadDialog onPressed: VersionSelector.openDownloadDialog
)); ));
return items; return items;
} }
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem( MenuFlyoutItem _createVersionItem(FortniteVersion version) => MenuFlyoutItem(
text: Listener( text: Listener(
onPointerDown: (event) async { onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) { if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
@@ -101,7 +143,7 @@ class _VersionSelectorState extends State<VersionSelector> {
await _openVersionOptions(version); await _openVersionOptions(version);
}, },
child: Text(version.content.toString()) child: Text(version.name)
), ),
trailing: IconButton( trailing: IconButton(
onPressed: () => _openVersionOptions(version), onPressed: () => _openVersionOptions(version),
@@ -109,14 +151,14 @@ class _VersionSelectorState extends State<VersionSelector> {
FluentIcons.more_vertical_24_regular FluentIcons.more_vertical_24_regular
) )
), ),
onPressed: () => _gameController.selectedVersion = version onPressed: () => _gameController.selectedVersion.value = version
); );
Future<void> _openVersionOptions(FortniteVersion version) async { Future<void> _openVersionOptions(FortniteVersion version) async {
final result = await _flyoutController.showFlyout<_ContextualOption?>( final result = await _flyoutController.showFlyout<_ContextualOption?>(
builder: (context) => MenuFlyout( builder: (context) => MenuFlyout(
items: _ContextualOption.values items: _ContextualOption.values
.map((entry) => _createOption(context, entry)) .map((entry) => _createOption(entry))
.toList() .toList()
), ),
barrierDismissible: true, barrierDismissible: true,
@@ -139,8 +181,19 @@ class _VersionSelectorState extends State<VersionSelector> {
launchUrl(version.location.uri) launchUrl(version.location.uri)
.onError((error, stackTrace) => _onExplorerError()); .onError((error, stackTrace) => _onExplorerError());
break; break;
case _ContextualOption.modify:
if(!mounted){
return;
}
if(close) {
Navigator.of(context).pop();
}
await VersionSelector.openImportDialog(version);
break;
case _ContextualOption.delete: case _ContextualOption.delete:
final result = await _openDeleteDialog(context, version) ?? false; final result = await _openDeleteDialog(version) ?? false;
if(!mounted || !result){ if(!mounted || !result){
return; return;
} }
@@ -160,7 +213,7 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
} }
MenuFlyoutItem _createOption(BuildContext context, _ContextualOption entry) { MenuFlyoutItem _createOption(_ContextualOption entry) {
return MenuFlyoutItem( return MenuFlyoutItem(
text: Text(entry.translatedName), text: Text(entry.translatedName),
onPressed: () => Navigator.of(context).pop(entry) onPressed: () => Navigator.of(context).pop(entry)
@@ -168,11 +221,15 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
bool _onExplorerError() { bool _onExplorerError() {
showRebootInfoBar(translations.missingVersion); showRebootInfoBar(
translations.missingVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
return false; return false;
} }
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) { Future<bool?> _openDeleteDialog(FortniteVersion version) {
return showRebootDialog<bool>( return showRebootDialog<bool>(
builder: (context) => ContentDialog( builder: (context) => ContentDialog(
content: Column( content: Column(
@@ -213,14 +270,14 @@ class _VersionSelectorState extends State<VersionSelector> {
enum _ContextualOption { enum _ContextualOption {
openExplorer, openExplorer,
delete; modify,
delete
}
extension _ContextualOptionExtension on _ContextualOption {
String get translatedName { String get translatedName {
switch(this) { return this == _ContextualOption.openExplorer ? translations.openInExplorer
case _ContextualOption.openExplorer: : this == _ContextualOption.modify ? translations.modify
return translations.openInExplorer; : translations.delete;
case _ContextualOption.delete:
return translations.delete;
}
} }
} }

View File

@@ -1,26 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
SettingTile buildVersionSelector({
required GlobalKey<OverlayTargetState> key
}) => SettingTile(
icon: Icon(
FluentIcons.play_24_regular
),
title: Text(translations.selectFortniteName),
subtitle: Text(translations.selectFortniteDescription),
contentWidth: null,
content: ConstrainedBox(
constraints: BoxConstraints(
minWidth: SettingTile.kDefaultContentWidth,
),
child: OverlayTarget(
key: key,
child: const VersionSelector(),
)
)
);

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Graphical User Interface for Project Reboot description: Graphical User Interface for Project Reboot
version: "10.0.5" version: "10.0.9"
publish_to: 'none' publish_to: 'none'
@@ -33,9 +33,6 @@ dependencies:
# Window management # Window management
window_manager: ^0.4.2 window_manager: ^0.4.2
# Extract zip archives (for example the reboot.zip)
archive: ^3.6.1
# Cryptographic functions # Cryptographic functions
bcrypt: ^1.1.3 bcrypt: ^1.1.3
pointycastle: ^3.9.1 pointycastle: ^3.9.1
@@ -43,17 +40,21 @@ 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
# Native utilities # Native utilities
archive: ^3.6.1
watcher: ^1.1.1
clipboard: ^0.1.3 clipboard: ^0.1.3
app_links: ^6.3.2 app_links: ^6.3.2
windows_taskbar: ^1.1.2 windows_taskbar: ^1.1.2
file_picker: ^8.1.2 file_picker: ^8.1.2
url_launcher: ^6.3.0 url_launcher: ^6.3.0
local_notifier: ^0.1.6 local_notifier: ^0.1.6
port_forwarder: ^1.0.0
# Server browser # Server browser
supabase_flutter: ^2.7.0 supabase_flutter: ^2.7.0

View File

@@ -55,6 +55,7 @@ begin
' Allow DLL injection', ' Allow DLL injection',
' The Reboot Launcher needs to inject DLLs into Fortnite to create the game server', ' The Reboot Launcher needs to inject DLLs into Fortnite to create the game server',
'Selecting the option below will add the Reboot Launcher to the Windows Exclusions list. ' + 'Selecting the option below will add the Reboot Launcher to the Windows Exclusions list. ' +
'If you are using another AntiVirus, it might be necessary to add an exclusion manually. ' +
'This is necessary because DLL injection is often detected as a virus, but is necessary to modify Fortnite. ' + 'This is necessary because DLL injection is often detected as a virus, but is necessary to modify Fortnite. ' +
'This option was designed for advanced users who want to manually manage the exclusions list on their machine. ' + 'This option was designed for advanced users who want to manually manage the exclusions list on their machine. ' +
'If you do not trust the Reboot Launcher, you can audit the source code at https://github.com/Auties00/reboot_launcher and build it from source.', 'If you do not trust the Reboot Launcher, you can audit the source code at https://github.com/Auties00/reboot_launcher and build it from source.',