20 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
Alessandro Autiero
d5e41ed646 10.0.5 2024-12-30 19:13:08 +01:00
Alessandro Autiero
9e20ec86e6 Merge remote-tracking branch 'origin/master' 2024-12-29 21:43:04 +01:00
Alessandro Autiero
004fc41292 Dependency 2024-12-29 21:42:54 +01:00
Alessandro Autiero
ee466df630 Update README.md 2024-12-24 21:52:06 +01:00
Alessandro Autiero
fdb1d694d9 Better moving system 2024-12-10 17:18:10 +01:00
Alessandro Autiero
0cfa4af236 10.0.4 2024-12-10 14:45:56 +01:00
Alessandro Autiero
d42946c44b 10.0.3 2024-12-09 22:28:24 +01:00
Alessandro Autiero
0a59a32c1b 10.0.2 2024-12-09 14:36:43 +01:00
115 changed files with 7932 additions and 5441 deletions

View File

@@ -1,16 +1,25 @@
![Banner](https://i.imgur.com/p0P4tcI.png)
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
Join our discord at https://discord.gg/reboot
Join our [Discord](https://discord.gg/rebootmp)
Install the launcher easily from the [releases](https://github.com/Auties00/Reboot-Launcher/releases/) section
## 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
- 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:
```
move.ps1
```
and provide the required parameters.

View File

@@ -1,98 +0,0 @@
param(
[Parameter(Mandatory=$true)]
[string]$UrlListPath, # Path to a text file with one URL per line
[Parameter(Mandatory=$true)]
[string]$BucketName, # Name of the R2 bucket
[Parameter(Mandatory=$true)]
[string]$AccessKey, # Your R2 access key
[Parameter(Mandatory=$true)]
[string]$SecretKey, # Your R2 secret key
[Parameter(Mandatory=$true)]
[string]$EndPointURL, # Your R2 endpoint URL, e.g. https://<account_id>.r2.cloudflarestorage.com
[Parameter(Mandatory=$false)]
[int]$MaxConcurrentConnections = 16, # Number of concurrent connections for each file download
[Parameter(Mandatory=$false)]
[int]$SplitCount = 16, # Number of segments to split the download into
[Parameter(Mandatory=$false)]
[string]$AwsRegion = "auto" # Region; often "auto" works for R2, but can be set if needed
)
# Set AWS environment variables for this session
$Env:AWS_ACCESS_KEY_ID = $AccessKey
$Env:AWS_SECRET_ACCESS_KEY = $SecretKey
$Env:AWS_REGION = $AwsRegion # If required, or leave as "auto"
# Read all URLs from file
$Urls = Get-Content $UrlListPath | Where-Object { $_ -and $_. Trim() -ne "" }
# Ensure aria2 is available
if (-not (Get-Command aria2c -ErrorAction SilentlyContinue)) {
Write-Error "aria2c not found in PATH. Please install aria2."
exit 1
}
# Ensure aws CLI is available
if (-not (Get-Command aws -ErrorAction SilentlyContinue)) {
Write-Error "aws CLI not found in PATH. Please install AWS CLI."
exit 1
}
function Process-Url {
param(
[string]$Url,
[string]$BucketName,
[string]$EndPointURL,
[int]$MaxConcurrentConnections,
[int]$SplitCount
)
# Extract the filename from the URL
$FileName = Split-Path -Leaf $Url
try {
Write-Host "Downloading: $Url"
# Use aria2c to download with multiple connections
& aria2c `
--max-connection-per-server=$MaxConcurrentConnections `
--split=$SplitCount `
--out=$FileName `
--check-certificate=false `
--header="Cookie: _c_t_c=1" `
$Url
if (!(Test-Path $FileName)) {
Write-Host "Failed to download $Url"
return
}
Write-Host "Uploading $FileName to R2 bucket: $BucketName"
& aws s3 cp $FileName "s3://$BucketName/$FileName" --endpoint-url $EndPointURL
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to upload $FileName to R2"
return
}
Write-Host "Upload successful. Deleting local file: $FileName"
Remove-Item $FileName -Force
Write-Host "Completed processing of $FileName."
} catch {
Write-Host "Error processing $Url"
Write-Host $_
}
}
# Process each URL sequentially here. If you'd like to run multiple URLs in parallel,
# you could replace the foreach loop with a ForEach-Object -Parallel block.
foreach ($Url in $Urls) {
Process-Url -Url $Url -BucketName $BucketName -EndPointURL $EndPointURL -MaxConcurrentConnections $MaxConcurrentConnections -SplitCount $SplitCount
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -7932,6 +7932,35 @@ express.post("/fortnite/api/game/v2/profile/*/client/SetHeroCosmeticVariants", a
res.end();
});
// 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
express.post("/fortnite/api/game/v2/profile/*/client/*", async (req, res) => {
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
description: Command Line Interface for Project Reboot
version: "1.0.0"
version: "10.0.7"
publish_to: 'none'
environment:
sdk: ">=2.19.0 <=3.3.4"
sdk: ">=3.0.0 <=3.5.3"
dependencies:
reboot_common:
path: ./../common
args: ^2.3.1
process_run: ^0.13.1
tint: ^2.0.1
interact_cli: ^2.4.0
args: ^2.6.0
version: ^3.0.2
dependency_overrides:
xml: ^6.3.0

View File

@@ -1,8 +1,6 @@
export 'package:reboot_common/src/constant/backend.dart';
export 'package:reboot_common/src/constant/game.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_version.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/dll.dart';
export 'package:reboot_common/src/util/backend.dart';
export 'package:reboot_common/src/util/build.dart';
export 'package:reboot_common/src/util/dll.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/downloader.dart';
export 'package:reboot_common/src/util/os.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 kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1";
@@ -21,5 +23,12 @@ const List<String> kCannotConnectErrors = [
"Network failure when attempting to check platform restrictions",
"UOnlineAccountCommon::ForceLogout"
];
const String kGameFinishedLine = "PlayersLeft: 1";
const String kDisplayInitializedLine = "Display";
const String kGameFinishedLine = "TeamsLeft: 1";
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,9 +1,10 @@
enum InjectableDll {
console,
starfall,
reboot,
auth,
gameServer,
memoryLeak
}
extension InjectableDllVersionAware on InjectableDll {
bool get isVersionDependent => this == InjectableDll.reboot;
bool get isVersionDependent => this == InjectableDll.gameServer;
}

View File

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

View File

@@ -1,22 +1,20 @@
import 'dart:io';
import 'package:version/version.dart';
class FortniteVersion {
Version content;
String name;
String gameVersion;
Directory location;
FortniteVersion.fromJson(json)
: content = Version.parse(json["content"]),
: name = json["name"],
gameVersion = json["gameVersion"],
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() => {
'content': content.toString(),
'name': name,
'gameVersion': gameVersion,
'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 'package:reboot_common/common.dart';
import 'package:version/version.dart';
class GameInstance {
final Version version;
final String version;
final bool host;
final int gamePid;
final int? launcherPid;
final int? eacPid;
final List<InjectableDll> injectedDlls;
final GameServerType? serverType;
final bool headless;
bool launched;
bool movedToVirtualDesktop;
bool tokenError;
bool killed;
GameInstance? child;
GameInstance({
required this.version,
required this.host,
required this.gamePid,
required this.launcherPid,
required this.eacPid,
required this.serverType,
required this.headless,
required this.child
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
}): tokenError = false, killed = false, launched = false, injectedDlls = [];
void kill() {
GameInstance? child = this;
@@ -35,20 +35,16 @@ class GameInstance {
}
void _kill() {
launched = true;
killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt);
if(launcherPid != null) {
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
}
if(eacPid != null) {
Process.killPid(eacPid!, ProcessSignal.sigabrt);
if(!killed) {
launched = true;
killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt);
if (launcherPid != null) {
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
}
if (eacPid != null) {
Process.killPid(eacPid!, ProcessSignal.sigabrt);
}
}
}
}
enum GameServerType {
headless,
virtualWindow,
window
}

View File

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

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:ini/ini.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:sync/semaphore.dart';
@@ -15,17 +15,152 @@ final Semaphore _semaphore = Semaphore();
String? _lastIp;
String? _lastPort;
Stream<ServerResult> startBackend({
required ServerType type,
required String host,
required String port,
required bool detached,
required void Function(String) onError
}) async* {
Process? process;
HttpServer? server;
try {
host = host.trim();
port = port.trim();
if(type != ServerType.local || port != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting);
}
if (host.isEmpty) {
yield ServerResult(ServerResultType.startMissingHostError);
return;
}
if (port.isEmpty) {
yield ServerResult(ServerResultType.startMissingPortError);
return;
}
final portNumber = int.tryParse(port);
if (portNumber == null) {
yield ServerResult(ServerResultType.startIllegalPortError);
return;
}
if ((type != ServerType.local || port != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
yield ServerResult(ServerResultType.startFreeingPort);
final result = await freeBackendPort();
if(!result) {
yield ServerResult(ServerResultType.startFreePortError);
return;
}
yield ServerResult(ServerResultType.startFreePortSuccess);
}
switch(type){
case ServerType.embedded:
process = await startEmbeddedBackend(detached, onError: onError);
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(process: process));
break;
case ServerType.remote:
yield ServerResult(ServerResultType.startPingingRemote);
final uriResult = await pingBackend(host, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.startPingError);
return;
}
server = await startRemoteBackendProxy(uriResult);
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
break;
case ServerType.local:
if(portNumber != kDefaultBackendPort) {
yield ServerResult(ServerResultType.startPingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.startPingError);
return;
}
server = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$port"));
yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server));
}
break;
}
yield ServerResult(ServerResultType.startPingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
if(uriResult == null) {
yield ServerResult(ServerResultType.startPingError);
process?.kill(ProcessSignal.sigterm);
server?.close(force: true);
return;
}
yield ServerResult(ServerResultType.startSuccess);
}catch(error, stackTrace) {
yield ServerResult(
ServerResultType.startError,
error: error,
stackTrace: stackTrace
);
process?.kill(ProcessSignal.sigterm);
server?.close(force: true);
}
}
Stream<ServerResult> stopBackend({required ServerType type, required ServerImplementation? implementation}) async* {
yield ServerResult(ServerResultType.stopping);
try{
switch(type){
case ServerType.embedded:
final process = implementation?.process;
if(process != null) {
Process.killPid(process.pid, ProcessSignal.sigterm);
}
break;
case ServerType.remote:
await implementation?.server?.close(force: true);
break;
case ServerType.local:
await implementation?.server?.close(force: true);
break;
}
yield ServerResult(ServerResultType.stopSuccess);
}catch(error, stackTrace){
yield ServerResult(
ServerResultType.stopError,
error: error,
stackTrace: stackTrace
);
}
}
Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
final process = await startProcess(
executable: backendStartExecutable,
window: detached,
);
process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
var killed = false;
process.stdError.listen((error) {
log("[BACKEND] Error: $error");
onError?.call(error);
if(!killed) {
log("[BACKEND] Error: $error");
killed = true;
process.kill(ProcessSignal.sigterm);
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;
}
@@ -61,7 +196,7 @@ Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
await request.close().timeout(const Duration(seconds: 10));
log("[BACKEND] Ping successful");
return uri;
}catch(error){
}catch(error) {
log("[BACKEND] Cannot ping backend: $error");
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
}
@@ -111,7 +246,7 @@ Future<void> writeMatchmakingIp(String text) async {
final splitIndex = text.indexOf(":");
final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
if(port.isBlank) {
if(port.isBlankOrEmpty) {
port = kDefaultGameServerPort;
}

View File

@@ -1,396 +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 {
final running = await _isAriaRunning();
if(running) {
await killProcessByPort(_ariaPort);
}
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe";
}
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"
],
window: false
);
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}"
]
};
await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return true;
}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));
}catch(error) {
throw "Stop failed (${error})";
}
}
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,72 +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';
bool _watcher = false;
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> downloadCriticalDll(String name, String outputPath) async {
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;
}
Stream<String> watchDlls() async* {
if(_watcher) {
return;
}
_watcher = true;
await for(final event in dllsDirectory.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (event.path.endsWith(".dll")) {
yield event.path;
}
}
}

View File

@@ -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"));
}
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 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
import 'package:synchronized/extension.dart';
final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1);
bool enableLoggingToConsole = true;
File _createLoggingFile() {
final file = File("${installationDirectory.path}\\launcher.log");
@@ -18,12 +18,14 @@ File _createLoggingFile() {
void log(String message) async {
try {
await _semaphore.acquire();
print(message);
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
if(enableLoggingToConsole) {
print(message);
}
launcherLogFile.synchronized(() async {
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
});
}catch(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
shelf_web_socket: ^2.0.0
version: ^3.0.2
synchronized: ^3.3.0+3
dev_dependencies:
flutter_lints: ^5.0.0

View File

@@ -1,5 +1,19 @@
# 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
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.
### 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]
bUseSSL=false
ServerAddr="ws://127.0.0.1"
ServerPort=80
ServerPort=8080
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp
[OnlineSubsystemMcp.Xmpp Prod]
bUseSSL=false
ServerAddr="ws://127.0.0.1"
ServerPort=80
ServerPort=8080
# Forces fortnite to use the v1 party system to support lawinserver xmpp
[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
},
{
"accountId": "Player231",
"accountId": "Player809",
"status": "ACCEPTED",
"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
}
]

View File

@@ -82,13 +82,31 @@
"created": "2024-05-23T19:36:22.635Z"
},
{
"accountId": "Player231",
"accountId": "Player809",
"groups": [],
"mutual": 0,
"alias": "",
"note": "",
"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": [],

File diff suppressed because it is too large Load Diff

Binary file not shown.

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",
"addLocalBuildName": "Add a version from this PC's local storage",
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
"addVersion": "Add version",
"addVersion": "Import",
"downloadBuildName": "Download any version from the cloud",
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
"downloadBuildContent": "Download build",
"downloadVersion": "Download",
"cannotUpdateGameServer": "An error occurred while updating the game server: {error}",
"launchFortnite": "Launch Fortnite",
"closeFortnite": "Close Fortnite",
@@ -146,9 +147,9 @@
"defaultServerName": "Reboot Game Server",
"defaultServerDescription": "Just another server",
"downloadingDll": "Downloading {name} dll...",
"dllAlreadyExists": "The {name} was already downloaded",
"downloadDllSuccess": "The {name} dll was downloaded successfully",
"downloadDllError": "An error occurred while downloading {name}: {error}",
"downloadDllAntivirus": "The {name} dll was deleted: your antivirus({antivirus}) might have flagged it",
"downloadDllRetry": "Retry",
"uncaughtErrorMessage": "An uncaught error was thrown: {error}",
"launchingGameServer": "Launching the game server...",
@@ -212,11 +213,11 @@
"selectBuild": "Select a fortnite version",
"fetchingBuilds": "Fetching builds and disks...",
"unknownError": "Unknown error",
"unknown": "unknown",
"downloadVersionError": "Cannot download version: {error}",
"downloadedVersion": "The download was completed successfully!",
"download": "Download",
"downloading": "Downloading...",
"allocatingSpace": "Allocating disk space...",
"startingDownload": "Starting download...",
"extracting": "Extracting...",
"buildProgress": "{progress}%",
@@ -237,13 +238,13 @@
"startGame": "Start fortnite",
"stopGame": "Close fortnite",
"waitingForGameServer": "Waiting for the game server to boot up...",
"gameServerStartWarning": "The game server was started successfully, but Reboot didn't load",
"gameServerStartWarning": "Unsupported version: the game server crashed while setting up the server",
"gameServerStartLocalWarning": "The game server was started successfully, but other players can't join",
"gameServerStarted": "The game server was started successfully",
"gameClientStarted": "The game client was started successfully",
"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",
"checkGameServerFixAction": "Fix",
"checkGameServerFixMessage": "The game server was started successfully, but other players can't join yet as port {port} isn't open",
"checkGameServerFixAction": "Learn more",
"infoName": "Info",
"emptyVersionName": "Empty version name",
"versionAlreadyExists": "This version already exists",
@@ -259,7 +260,8 @@
"emptyURL": "Empty update URL",
"missingVersionError": "Download or select a version before starting Fortnite",
"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}",
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
@@ -283,9 +285,9 @@
"infoVideoName": "Tutorial",
"infoVideoDescription": "Show the tutorial again in the launcher",
"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",
"dllDeletedPrimaryAction": "Try again",
"dllDeletedPrimaryAction": "Disable Antivirus",
"clickKey": "Waiting for a key to be registered",
"settingsLogsName": "Export logs",
"settingsLogsDescription": "Exports an archive containing all the logs produced by the launcher",
@@ -307,11 +309,8 @@
"quizZeroTriesLeft": "zero tries",
"quizOneTryLeft": "one try",
"quizTwoTriesLeft": "two tries",
"gameServerTypeName": "Type",
"gameServerTypeDescription": "The type of game server to use",
"gameServerTypeHeadless": "Background process",
"gameServerTypeVirtualWindow": "Virtual window",
"gameServerTypeWindow": "Normal window",
"gameServerTypeName": "Headless",
"gameServerTypeDescription": "Disables game rendering to save resources",
"localBuild": "This PC",
"githubArchive": "Cloud archive",
"all": "All",
@@ -375,5 +374,14 @@
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
"gameResetDefaultsContent": "Reset",
"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

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

View File

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

View File

@@ -7,18 +7,22 @@ import 'package:get_storage/get_storage.dart';
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
import 'package:version/version.dart';
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 {
static const String storageName = "v2_dll_storage";
static const String storageName = "v3_dll_storage";
late final GetStorage? _storage;
late final String originalDll;
late final TextEditingController gameServerDll;
late final TextEditingController customGameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final Rx<UpdateTimer> timer;
late final TextEditingController beforeS20Mirror;
@@ -26,28 +30,29 @@ class DllController extends GetxController {
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
late final Map<InjectableDll, StreamSubscription?> _subscriptions;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot);
customGameServerDll = _createController("game_server", InjectableDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.starfall);
backendDll = _createController("backend", InjectableDll.auth);
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
beforeS20Mirror = TextEditingController(text: _storage?.read("update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text));
beforeS20Mirror = TextEditingController(text: _storage?.read("before_s20_update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("before_s20_update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("after_s20_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("after_s20_update_url", aboveS20Mirror.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
_subscriptions = {};
}
TextEditingController _createController(String key, InjectableDll dll) {
@@ -57,9 +62,9 @@ class DllController extends GetxController {
}
void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot);
customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.starfall);
backendDll.text = getDefaultDllPath(InjectableDll.auth);
}
void resetServer() {
@@ -74,19 +79,11 @@ class DllController extends GetxController {
}
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
if(_updater != null) {
return await _updater!;
}
final result = _updateGameServerDll(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateGameServerDll(bool force, bool silent) async {
InfoBarEntry? infoBarEntry;
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
@@ -97,6 +94,7 @@ class DllController extends GetxController {
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
@@ -107,14 +105,25 @@ class DllController extends GetxController {
duration: null
);
}
await Future.wait(
final result = await Future.wait(
[
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text),
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text, false),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text, true),
Future.delayed(const Duration(seconds: 1))
.then((_) => true)
],
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;
status.value = UpdateStatus.success;
infoBarEntry?.close();
@@ -125,6 +134,7 @@ class DllController extends GetxController {
duration: infoBarShortDuration
);
}
_listenToFileEvents(InjectableDll.gameServer);
return true;
}catch(message) {
infoBarEntry?.close();
@@ -132,106 +142,222 @@ class DllController extends GetxController {
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
final completer = Completer<bool>();
infoBarEntry = showRebootInfoBar(
translations.downloadDllError(error.toString(), "reboot.dll"),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(false),
action: Button(
onPressed: () async {
infoBarEntry?.close();
updateGameServerDll(
final result = updateGameServerDll(
force: true,
silent: silent
);
completer.complete(result);
},
child: Text(translations.downloadDllRetry),
)
);
return false;
}finally {
_updater = null;
return completer.future;
}
}
(File, bool) getInjectableData(Version version, InjectableDll dll) {
(File, bool) getInjectableData(String version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.reboot:
case InjectableDll.gameServer:
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:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.starfall:
case InjectableDll.auth:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memoryLeak:
final memoryFile = File(memoryLeakDll.text);
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
}
}
String getDefaultDllPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
bool _isS20(String version) {
try {
return Version.parse(version).major >= 20;
} on FormatException catch(_) {
return version.trim().startsWith("20.");
}
}
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $filePath(silent: $silent)");
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
TextEditingController getDllEditingController(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
return unrealEngineConsoleDll;
case InjectableDll.auth:
return backendDll;
case InjectableDll.gameServer:
return customGameServerDll;
case InjectableDll.memoryLeak:
return memoryLeakDll;
}
}
String getDefaultDllPath(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth:
return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak:
return "${dllsDirectory.path}\\memory.dll";
}
}
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
InfoBarEntry? entry;
try {
if (fileName.contains("reboot")) {
log("[DLL] Downloading reboot.dll...");
return await updateGameServerDll(
silent: silent
);
if (dll == InjectableDll.gameServer) {
return await updateGameServerDll(silent: silent);
}
if(!force && File(filePath).existsSync()) {
log("[DLL] File already exists");
log("[DLL] $dll already exists");
_listenToFileEvents(dll);
return true;
}
log("[DLL] Downloading $dll...");
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
log("[DLL] Showing dialog while downloading $dll...");
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}else {
log("[DLL] Not showing dialog while downloading $dll...");
}
await downloadCriticalDll(fileName, 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();
if(!silent) {
log("[DLL] Showing success dialog for $dll");
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}else {
log("[DLL] Not showing success dialog for $dll");
}
_listenToFileEvents(dll);
return true;
}catch(message) {
log("[DLL] Error: $message");
log("[DLL] An error occurred while downloading $dll: $message");
entry?.close();
var error = message.toString();
error =
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
final completer = Completer<bool>();
await showRebootInfoBar(
translations.downloadDllError(error.toString(), fileName),
translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
onDismissed: () => completer.complete(false),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
final result = await download(dll, filePath, silent: silent, force: force);
completer.complete(result);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
return completer.future;
}
}
Future<void> downloadAndGuardDependencies() async {
for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
if(path.equals(controller.text, defaultPath)) {
await download(injectable, controller.text);
}
}
}
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;
}
if(path.equals(filePath, defaultPath)) {
Get.find<GameController>()
.instance
.value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
_updateInput(injectable);
}
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:convert';
import 'dart:math';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:version/version.dart';
class GameController extends GetxController {
static const String storageName = "v2_game_storage";
static const String storageName = "v3_game_storage";
late final GetStorage? _storage;
late final TextEditingController username;
late final TextEditingController password;
late final TextEditingController customLaunchArgs;
late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion;
late final Rxn<FortniteVersion> selectedVersion;
late final RxBool started;
late final Rxn<GameInstance> instance;
@@ -28,8 +30,8 @@ class GameController extends GetxController {
versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions());
final decodedSelectedVersionName = _storage?.read("version");
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName);
_selectedVersion = Rxn(decodedSelectedVersion);
selectedVersion = Rxn(decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName));
selectedVersion.listen((version) => _storage?.write("version", version?.name));
username = TextEditingController(
text: _storage?.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage?.write("username", username.text));
@@ -46,26 +48,42 @@ class GameController extends GetxController {
password.text = "";
customLaunchArgs.text = "";
versions.value = [];
_selectedVersion.value = null;
selectedVersion.value = null;
instance.value = null;
}
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) {
var empty = versions.value.isEmpty;
versions.update((val) => val?.add(version));
if(empty){
selectedVersion = version;
}
selectedVersion.value = version;
}
void removeVersion(FortniteVersion version) {
versions.update((val) => val?.remove(version));
if (selectedVersion == version || hasNoVersions) {
selectedVersion = null;
final index = versions.value.indexOf(version);
versions.update((val) => val?.removeAt(index));
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;
FortniteVersion? get selectedVersion => _selectedVersion();
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));
}
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';
class HostingController extends GetxController {
static const String storageName = "v2_hosting_storage";
static const String storageName = "v3_hosting_storage";
late final GetStorage? _storage;
late final String uuid;
@@ -26,7 +26,7 @@ class HostingController extends GetxController {
late final FocusNode passwordFocusNode;
late final RxBool showPassword;
late final RxBool discoverable;
late final Rx<GameServerType> type;
late final RxBool headless;
late final RxBool autoRestart;
late final RxBool started;
late final RxBool published;
@@ -54,8 +54,8 @@ class HostingController extends GetxController {
passwordFocusNode = FocusNode();
discoverable = RxBool(_storage?.read("discoverable") ?? false);
discoverable.listen((value) => _storage?.write("discoverable", value));
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index));
type.listen((value) => _storage?.write("type", value.index));
headless = RxBool(_storage?.read("headless") ?? true);
headless.listen((value) => _storage?.write("headless", value));
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
autoRestart.listen((value) => _storage?.write("auto_restart", value));
started = RxBool(false);
@@ -165,7 +165,7 @@ class HostingController extends GetxController {
showPassword.value = false;
discoverable.value = false;
instance.value = null;
type.value = GameServerType.headless;
headless.value = true;
autoRestart.value = true;
customLaunchArgs.text = "";
}

View File

@@ -6,14 +6,14 @@ import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
class SettingsController extends GetxController {
static const String storageName = "v2_settings_storage";
static const String storageName = "v3_settings_storage";
late final GetStorage? _storage;
late final RxString language;

View File

@@ -1,7 +1,7 @@
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
@@ -126,7 +126,7 @@ class ProgressDialog extends AbstractDialog {
header: InfoLabel(
label: text,
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
padding: const EdgeInsets.symmetric(vertical: 16.0),
width: double.infinity,
child: const ProgressBar()
),

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/widget/message/onboard.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
abstract class RebootPage extends StatefulWidget {

View File

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

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:reboot_common/common.dart';
@@ -9,22 +10,24 @@ const Duration _timeout = Duration(seconds: 5);
Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
final completer = Completer<bool>();
final start = DateTime.now();
(() async {
while (!completer.isCompleted && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds) {
final result = await pingGameServer(address);
if(result) {
completer.complete(true);
}else {
await Future.delayed(_timeout);
}
}
if(!completer.isCompleted) {
completer.complete(false);
}
})();
_pingGameServerOrTimeout(completer, start, timeout, address);
return completer;
}
Future<void> _pingGameServerOrTimeout(Completer<bool> completer, DateTime start, Duration timeout, String address) async {
while (!completer.isCompleted && max(DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch, 0) < timeout.inMilliseconds) {
final result = await pingGameServer(address);
if(result) {
completer.complete(true);
}else {
await Future.delayed(_timeout);
}
}
if(!completer.isCompleted) {
completer.complete(false);
}
}
Future<bool> pingGameServer(String address) async {
final split = address.split(":");
var hostname = split[0];

View File

@@ -47,448 +47,60 @@ Future<String?> openFilePicker(String extension) async {
bool get isDarkMode =>
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 {
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
}
class WindowsDisk {
static final String _nullTerminator = String.fromCharCode(0);
final String path;
final int freeBytesAvailable;
final int totalNumberOfBytes;
const WindowsDisk._internal(this.path, this.freeBytesAvailable, this.totalNumberOfBytes);
static List<WindowsDisk> available() {
final buffer = malloc.allocate<Utf16>(MAX_PATH);
try {
final length = GetLogicalDriveStrings(MAX_PATH, buffer);
if (length == 0) {
return [];
}
return buffer.toDartString(length: length)
.split(_nullTerminator)
.where((drive) => drive.length > 1)
.map((driveName) {
final freeBytesAvailable = calloc<Uint64>();
final totalNumberOfBytes = calloc<Uint64>();
final totalNumberOfFreeBytes = calloc<Uint64>();
try {
GetDiskFreeSpaceEx(
driveName.toNativeUtf16(),
freeBytesAvailable,
totalNumberOfBytes,
totalNumberOfFreeBytes
);
return WindowsDisk._internal(
driveName,
freeBytesAvailable.value,
totalNumberOfBytes.value
);
} finally {
calloc.free(freeBytesAvailable);
calloc.free(totalNumberOfBytes);
calloc.free(totalNumberOfFreeBytes);
}
})
.toList(growable: false);
} finally {
calloc.free(buffer);
}
}
@override
String toString() {
return 'WindowsDisk{path: $path, freeBytesAvailable: $freeBytesAvailable, totalNumberOfBytes: $totalNumberOfBytes}';
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,17 +7,15 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart';
import 'package:port_forwarder/port_forwarder.dart';
import 'package:reboot_common/common.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/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.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:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -46,7 +44,6 @@ class _LaunchButtonState extends State<LaunchButton> {
InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation;
Completer? _pingOperation;
IVirtualDesktop? _virtualDesktop;
@override
Widget build(BuildContext context) => Align(
@@ -74,17 +71,19 @@ class _LaunchButtonState extends State<LaunchButton> {
if (host ? _hostingController.started() : _gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop(
reason: _StopReason.normal
reason: _StopReason.normal,
host: host
);
return;
}
final version = _gameController.selectedVersion;
final version = _gameController.selectedVersion.value;
log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
if(version == null){
log("[${host ? 'HOST' : 'GAME'}] No version selected");
_onStop(
reason: _StopReason.missingVersionError
reason: _StopReason.missingVersionError,
host: host
);
return;
}
@@ -94,45 +93,47 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${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;
}
}
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})...");
final backendResult = _backendController.started() || await _backendController.toggleInteractive();
final backendResult = _backendController.started() || await _backendController.toggle();
if(!backendResult){
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop(
reason: _StopReason.backendError
reason: _StopReason.backendError,
host: host
);
return;
}
log("[${host ? 'HOST' : 'GAME'}] Backend works");
final serverType = _hostingController.type.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
final headless = _hostingController.headless.value;
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");
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance);
final result = await _startGameProcesses(version, host, headless, linkedHostingInstance);
final started = host ? _hostingController.started() : _gameController.started();
if(!started) {
result?.kill();
return;
}
if(host || linkedHostingInstance != null) {
if (_dllController.gameServerPort.text == kDefaultBackendPort.toString()) {
_onStop(
reason: _StopReason.gameServerPortError,
host: host
);
return;
}
}
if(!host) {
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null);
_showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null);
}else {
_showLaunchingGameServerWidget();
}
@@ -140,18 +141,20 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop(
reason: _StopReason.corruptedVersionError,
error: exception.toString(),
stackTrace: stackTrace
stackTrace: stackTrace,
host: host
);
} catch (exception, stackTrace) {
_onStop(
reason: _StopReason.unknownError,
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...");
if(host){
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
@@ -175,7 +178,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
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...");
_setStarted(true, true);
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
@@ -185,7 +188,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _askForAutomaticGameServer(bool host) async {
if (host ? !_hostingController.started() : !_gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop(reason: _StopReason.normal);
_onStop(reason: _StopReason.normal, host: host);
return false;
}
@@ -209,19 +212,10 @@ class _LaunchButtonState extends State<LaunchButton> {
return result;
}
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher...");
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable);
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);
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
final eacProcess = await _createPausedProcess(version, host, kEacExe);
final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
if(gameProcess == null) {
log("[${host ? 'HOST' : 'GAME'}] No game process was created");
return null;
@@ -229,11 +223,12 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance(
version: version.content,
version: version.gameVersion,
host: host,
gamePid: gameProcess,
launcherPid: launcherProcess,
eacPid: eacProcess,
serverType: host ? hostType : null,
headless: host && headless,
child: linkedHosting
);
if(host){
@@ -242,27 +237,65 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{
_gameController.instance.value = instance;
}
await _injectOrShowError(InjectableDll.starfall, host);
await _injectOrShowError(InjectableDll.auth, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game 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...");
final gameArgs = createRebootArgs(
host ? _hostingController.accountUsername.text : _gameController.username.text,
host ? _hostingController.accountPassword.text :_gameController.password.text,
host ? _hostingController.accountPassword.text : _gameController.password.text,
host,
hostType,
headless,
false,
host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
);
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
final gameProcess = await startProcess(
executable: executable,
executable: shippingExecutables.first,
args: gameArgs,
useTempBatch: false,
name: "${version.content}-${host ? 'HOST' : 'GAME'}",
name: "${version.gameVersion}-${host ? 'HOST' : 'GAME'}",
environment: {
"OPENSSL_ia32cap": "~0x20000000"
}
@@ -273,26 +306,26 @@ class _LaunchButtonState extends State<LaunchButton> {
handleGameOutput(
line: line,
host: host,
onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError),
onShutdown: () => _onStop(reason: _StopReason.normal, host: host),
onTokenError: () => _onStop(reason: _StopReason.tokenError, host: host),
onBuildCorrupted: () {
if(instance == null) {
return;
}else if(!instance.launched) {
_onStop(reason: _StopReason.corruptedVersionError);
_onStop(reason: _StopReason.corruptedVersionError, host: host);
}else {
_onStop(reason: _StopReason.crash);
_onStop(reason: _StopReason.crash, host: host);
}
},
onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version),
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
onMatchEnd: () => _onMatchEnd(version)
);
}
gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
gameProcess.stdError.listen((line) => onGameOutput(line, true));
gameProcess.exitCode.then((_) async {
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
instance?.killed = true;
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
_onStop(
reason: _StopReason.exitCode,
@@ -302,60 +335,37 @@ class _LaunchButtonState extends State<LaunchButton> {
return gameProcess.pid;
}
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async {
if (file == null) {
Future<int?> _createPausedProcess(FortniteVersion version, bool host, String executableName) async {
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;
}
final process = await startProcess(
executable: file,
executable: executables.first,
useTempBatch: false,
name: "${version.content}-${basenameWithoutExtension(file.path)}",
name: "${version.gameVersion}-${basenameWithoutExtension(executables.first.path)}",
environment: {
"OPENSSL_ia32cap": "~0x20000000"
}
);
log("[${host ? 'HOST' : 'GAME'}] Started paused $executableName: $process");
final pid = process.pid;
suspend(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) {
if(_hostingController.autoRestart.value) {
final notification = LocalNotification(
@@ -398,6 +408,9 @@ class _LaunchButtonState extends State<LaunchButton> {
if(instance != null && !instance.launched) {
instance.launched = true;
instance.tokenError = false;
if(_isChapterOne(instance.version)) {
await _injectOrShowError(InjectableDll.memoryLeak, host);
}
if(!host){
await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected();
@@ -406,12 +419,20 @@ class _LaunchButtonState extends State<LaunchButton> {
if(gameServerPort != null) {
await killProcessByPort(gameServerPort);
}
await _injectOrShowError(InjectableDll.reboot, host);
await _injectOrShowError(InjectableDll.gameServer, host);
_onGameServerInjected();
}
}
}
bool _isChapterOne(String version) {
try {
return Version.parse(version).major < 10;
} on FormatException catch(_) {
return true;
}
}
void _onGameClientInjected() {
_gameClientInfoBar?.close();
showRebootInfoBar(
@@ -428,36 +449,30 @@ class _LaunchButtonState extends State<LaunchButton> {
_gameClientInfoBar?.close();
}
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
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();
if (!localPingResult) {
showRebootInfoBar(
translations.gameServerStartWarning,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
final started = await _checkLocalGameServer(gameServerPort);
if(!started) {
if (_hostingController.instance.value?.killed != true) {
showRebootInfoBar(
translations.gameServerStartWarning,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}
return;
}
_backendController.joinLocalhost();
final accessible = await _checkGameServer(theme, gameServerPort);
final accessible = await _checkPublicGameServer(gameServerPort);
if (!accessible) {
showRebootInfoBar(
translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration
translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
child: Text(translations.checkGameServerFixAction),
),
);
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 {
_gameServerInfoBar = showRebootInfoBar(
translations.checkingGameServer,
@@ -484,50 +522,72 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
final publicIp = await Ipify.ipv4();
final available = await pingGameServer("$publicIp:$gameServerPort");
if(available) {
var pingOperation = await pingGameServerOrTimeout(
"$publicIp:$gameServerPort",
const Duration(seconds: 10)
);
_pingOperation = pingOperation;
var publicPingResult = await pingOperation.future;
if (publicPingResult) {
_gameServerInfoBar?.close();
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",
const Duration(days: 365)
const Duration(seconds: 10)
);
this._pingOperation = pingOperation;
_gameServerInfoBar = showRebootInfoBar(
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;
_pingOperation = pingOperation;
publicPingResult = await pingOperation.future;
_gameServerInfoBar?.close();
return result;
}finally {
return publicPingResult;
}catch(_) {
_gameServerInfoBar?.close();
return false;
}
}
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
if(host == null) {
Future<void> _onStop({
required _StopReason reason,
required bool host,
String? error,
StackTrace? stackTrace,
bool interactive = true
}) async {
if(host) {
try {
_pingOperation?.complete(false);
}catch(_) {
// Ignore: might be running, don't bother checking
} catch (_) {
// Ignore: might have been already terminated, don't bother checking
} finally {
_pingOperation = null;
}
await _operation?.cancel();
_operation = null;
_backendController.cancelInteractive();
}
host = host ?? widget.host;
await _operation?.cancel();
_operation = null;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){
@@ -536,15 +596,6 @@ class _LaunchButtonState extends State<LaunchButton> {
_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'}] Caller: ${StackTrace.current}");
if(host) {
@@ -560,98 +611,121 @@ class _LaunchButtonState extends State<LaunchButton> {
if(child != null) {
await _onStop(
reason: reason,
host: child.serverType != null
host: child.host,
error: error,
stackTrace: stackTrace,
interactive: false
);
}
_setStarted(host, false);
WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) {
_gameServerInfoBar?.close();
}else {
_gameClientInfoBar?.close();
}
});
switch(reason) {
case _StopReason.backendError:
case _StopReason.matchmakerError:
case _StopReason.normal:
break;
case _StopReason.missingVersionError:
showRebootInfoBar(
translations.missingVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.missingExecutableError:
showRebootInfoBar(
translations.missingExecutableError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.exitCode:
if(instance != null && !instance.launched) {
if(interactive) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) {
_gameServerInfoBar?.close();
}else {
_gameClientInfoBar?.close();
}
});
switch(reason) {
case _StopReason.backendError:
case _StopReason.matchmakerError:
case _StopReason.normal:
break;
case _StopReason.missingVersionError:
showRebootInfoBar(
translations.corruptedVersionError,
translations.missingVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
}
break;
case _StopReason.corruptedVersionError:
showRebootInfoBar(
translations.corruptedVersionError,
break;
case _StopReason.missingExecutableError:
showRebootInfoBar(
translations.missingExecutableError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
break;
case _StopReason.corruptedDllError:
showRebootInfoBar(
translations.corruptedDllError(error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.missingCustomDllError:
showRebootInfoBar(
translations.missingCustomDllError(error!),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.tokenError:
_backendController.stop();
showRebootInfoBar(
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
);
break;
case _StopReason.multipleExecutablesError:
showRebootInfoBar(
translations.multipleExecutablesError(error ?? translations.unknown),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? "game server" : "client"),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError:
showRebootInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
);
break;
case _StopReason.exitCode:
if(instance != null && !instance.launched) {
final injectedDlls = instance.injectedDlls;
showRebootInfoBar(
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
}
break;
case _StopReason.corruptedVersionError:
final injectedDlls = instance?.injectedDlls ?? [];
showRebootInfoBar(
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
break;
case _StopReason.corruptedDllError:
showRebootInfoBar(
translations.corruptedDllError(error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.missingCustomDllError:
showRebootInfoBar(
translations.missingCustomDllError(error!),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.tokenError:
_backendController.stop(interactive: false);
final injectedDlls = instance?.injectedDlls;
showRebootInfoBar(
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? translations.gameServer : translations.client),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError:
showRebootInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
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");
final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
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");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: hosting
);
return;
}
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable
.name}...");
await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
@@ -692,29 +762,38 @@ 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}...");
final (file, customDll) = _dllController.getInjectableData(version, injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) {
try {
await file.readAsBytes();
log("[${host ? 'HOST' : 'GAME'}] Path exists");
return file;
}catch(_) {
}
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
if(customDll) {
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: host
);
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _dllController.downloadCriticalDllInteractive(file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(version, injectable, host, true);
final result = await _dllController.download(injectable, file.path, force: true);
if(result) {
log("[${host ? 'HOST' : 'GAME'}] Downloaded critical dll");
return file;
}
_onStop(reason: _StopReason.normal, host: host);
return null;
}
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
@@ -723,7 +802,7 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
@@ -741,9 +820,9 @@ class _LaunchButtonState extends State<LaunchButton> {
onPressed: () async {
_backendController.joinLocalhost();
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();
_showLaunchingGameClientWidget(version, hostType, true);
_showLaunchingGameClientWidget(version, headless, true);
}
},
child: Text(translations.startGameServer),
@@ -758,6 +837,7 @@ enum _StopReason {
normal,
missingVersionError,
missingExecutableError,
multipleExecutablesError,
corruptedVersionError,
missingCustomDllError,
corruptedDllError,
@@ -765,6 +845,7 @@ enum _StopReason {
matchmakerError,
tokenError,
unknownError,
gameServerPortError,
exitCode,
crash;

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,7 @@ import 'package:email_validator/email_validator.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{

View File

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

View File

@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
@@ -9,12 +8,11 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
class BrowsePage extends RebootPage {
const BrowsePage({Key? key}) : super(key: key);
@@ -182,7 +180,7 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
case _Filter.accessible:
return element.password == null;
case _Filter.playable:
return _gameController.getVersionByName(element.version) != null;
return _gameController.getVersionByGame(element.version) != null;
}
}).toList();
final sort = _sort.value;
@@ -211,10 +209,18 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
icon: Icon(
hasPassword ? FluentIcons.lock : FluentIcons.globe
),
title: Text("${_formatName(entry)}${entry.author}"),
subtitle: Text("${_formatDescription(entry)}${_formatVersion(entry)}"),
title: Text(
"${_formatName(entry)}${entry.author}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
subtitle: Text(
"${_formatDescription(entry)}${_formatVersion(entry)}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
content: Button(
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry),
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
)
);

View File

@@ -10,22 +10,21 @@ import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/dll.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_suggestion.dart';
import 'package:reboot_launcher/src/page/pages.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/widget/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart';
import 'package:reboot_launcher/src/widget/title_bar.dart';
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/fluent/profile_tile.dart';
import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart';
@@ -33,7 +32,6 @@ final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
class HomePage extends StatefulWidget {
static const double kDefaultPadding = 12.0;
static const double kTitleBarHeight = 32;
const HomePage({Key? key}) : super(key: key);
@@ -43,6 +41,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
final BackendController _backendController = Get.find<BackendController>();
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
@@ -76,6 +75,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
lastPage = index;
_pageController.jumpToPage(index);
pagesController.add(null);
});
}
@@ -93,7 +93,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
final uuid = uri.host;
final server = _hostingController.findServerById(uuid);
if(server != null) {
_backendController.joinServerInteractive(_hostingController.uuid, server);
_backendController.joinServer(_hostingController.uuid, server);
}else {
showRebootInfoBar(
translations.noServerFound,
@@ -134,40 +134,50 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
dllsDirectory.createSync(recursive: true);
}
final dummy = Version.parse("1");
final dummyS20 = Version.parse("20");
for(final injectable in InjectableDll.values) {
_downloadDll(dummy, injectable);
if(injectable.isVersionDependent) {
_downloadDll(dummyS20, injectable);
}
}
watchDlls().listen((filePath) => showDllDeletedDialog(() {
_dllController.downloadCriticalDllInteractive(filePath);
}));
}
void _downloadDll(Version version, InjectableDll injectable) {
final (file, custom) = _dllController.getInjectableData(version, injectable);
if(!custom) {
_dllController.downloadCriticalDllInteractive(
file.path,
silent: false
);
}
_dllController.downloadAndGuardDependencies();
}
@override
void onWindowClose() async {
try {
await windowManager.hide();
}catch(error) {
log("[WINDOW] Cannot hide window: $error");
}
try {
await _hostingController.discardServer();
}catch(error) {
log("[HOSTING] Cannot discard server: $error");
}finally {
// Force closing because the backend might be running, but we want the process to exit
exit(0);
log("[HOSTING] Cannot discard server on exit: $error");
}
try {
if(_backendController.started.value) {
await _backendController.toggle();
}
}catch(error) {
log("[BACKEND] Cannot stop backend on exit: $error");
}
try {
_gameController.instance.value?.kill();
}catch(error) {
log("[GAME] Cannot stop game on exit: $error");
}
try {
_hostingController.instance.value?.kill();
}catch(error) {
log("[HOST] Cannot stop host on exit: $error");
}
try {
await stopDownloadServer();
}catch(error) {
log("[ARIA] Cannot stop aria server on exit: $error");
}
exit(0);
}
@override
@@ -255,62 +265,36 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
_focused.value = true;
}
@override
void onWindowEvent(String eventName) {
if(eventName != "move") {
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
}
}
@override
Widget build(BuildContext context) {
super.build(context);
_settingsController.language.value;
loadTranslations(context);
return Obx(() {
return Container(
return Container(
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: HomePage.kTitleBarHeight,
child: Row(
children: [
_backButton,
Expanded(child: _draggableArea),
WindowTitleBar(focused: _focused())
],
)
),
Expanded(
child: Navigator(
key: appNavigatorKey,
onPopPage: (page, data) => false,
pages: [
MaterialPage(
child: Overlay(
key: appOverlayKey,
initialEntries: [
OverlayEntry(
maintainState: true,
builder: (context) => Row(
children: [
_buildLateralView(),
_buildBody()
],
)
)
],
),
child: Navigator(
key: appNavigatorKey,
onPopPage: (page, data) => false,
pages: [
MaterialPage(
child: Overlay(
key: appOverlayKey,
initialEntries: [
OverlayEntry(
maintainState: true,
builder: (context) => Row(
children: [
_buildLateralView(),
_buildBody()
],
)
)
],
)
),
)
],
),
);
});
)
);
}
Widget _buildBody() => Expanded(
@@ -541,42 +525,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
);
}
Widget get _backButton => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0
)),
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.all(Border())
),
onPressed: appStack.isEmpty && !inDialog ? null : () {
if(inDialog) {
Navigator.of(appNavigatorKey.currentContext!).pop();
}else {
final lastPage = appStack.removeLast();
pageStack.remove(lastPage);
if (lastPage is int) {
hitBack = true;
pageIndex.value = lastPage;
} else {
Navigator.of(pageKey.currentContext!).pop();
}
}
pagesController.add(null);
},
child: const Icon(FluentIcons.back, size: 12.0),
)
);
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: windowManager.maximizeOrRestore,
onHorizontalDragStart: (_) => windowManager.startDragging(),
onVerticalDragStart: (_) => windowManager.startDragging()
);
Widget get _autoSuggestBox => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,

View File

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

View File

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

View File

@@ -1,15 +1,16 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.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/version/version_selector.dart';
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
@@ -34,6 +35,7 @@ class PlayPage extends RebootPage {
class _PlayPageState extends RebootPageState<PlayPage> {
final GameController _gameController = Get.find<GameController>();
final DllController _dllController = Get.find<DllController>();
@override
Widget? get button => LaunchButton(
@@ -44,7 +46,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
@override
List<SettingTile> get settings => [
buildVersionSelector(
VersionSelector.buildTile(
key: gameVersionOverlayTargetKey
),
_options,
@@ -81,6 +83,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
content: Button(
onPressed: () => showResetDialog(() {
_gameController.reset();
_dllController.resetGame();
}),
child: Text(translations.gameResetDefaultsContent),
)

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
@@ -6,14 +9,19 @@ import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.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 {
const SettingsPage({Key? key}) : super(key: key);
@@ -36,6 +44,7 @@ class SettingsPage extends RebootPage {
class _SettingsPageState extends RebootPageState<SettingsPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
int? _downloadFromMirrorId;
@override
Widget? get button => null;
@@ -56,23 +65,39 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
key: settingsConsoleDllInputKey,
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll,
onReset: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
await _dllController.download(InjectableDll.console, path, force: true);
settingsConsoleDllInputKey.currentState?.validate();
}
),
createFileSetting(
key: settingsAuthDllInputKey,
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.starfall);
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
_dllController.backendDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
await _dllController.download(InjectableDll.auth, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
),
createFileSetting(
key: settingsMemoryDllInputKey,
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll,
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
_dllController.memoryLeakDll.text = path;
await _dllController.download(InjectableDll.memoryLeak, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
),
_internalFilesServerType,
@@ -105,7 +130,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
}
_dllController.customGameServer.value = entry.key;
_dllController.infoBarEntry?.close();
if(!entry.key) {
_dllController.updateGameServerDll(
force: true
@@ -129,13 +153,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
@@ -172,18 +192,38 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
);
}else {
return createFileSetting(
key: settingsGameServerDllInputKey,
title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.gameServerDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.reboot);
_dllController.gameServerDll.text = path;
_dllController.downloadCriticalDllInteractive(path);
controller: _dllController.customGameServerDll,
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
_dllController.customGameServerDll.text = 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(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
@@ -197,13 +237,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
@@ -263,7 +299,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
text: Text(entry.text),
onPressed: () {
_dllController.timer.value = entry;
_dllController.infoBarEntry?.close();
_dllController.updateGameServerDll(
force: true
);

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More