10 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
77 changed files with 6793 additions and 3756 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](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!

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,60 +1,100 @@
import 'package:interact/interact.dart';
import 'package:reboot_cli/cli.dart';
import 'package:tint/tint.dart';
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: 'build', parameters: [], subCommands: [_buildImport, _buildDownload]);
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) {
_welcome();
void main(List<String> args) async {
enableLoggingToConsole = false;
useDefaultPath = true;
final parser = Parser(commands: [_build, _play, _host, _backend]);
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);
print(command);
_handleRootCommand(command);
await _handleRootCommand(command);
}
void _handleRootCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
case 'build':
_handleBuildCommand(call?.subCall);
Future<void> _handleRootCommand(CommandCall? command) async {
if(command == null) {
await _askRootCommand();
return;
}
switch (command.name) {
case 'versions':
await _handleBuildCommand(command.subCall);
break;
case 'play':
_handleBuildCommand(call?.subCall);
_handlePlayCommand(command.subCall);
break;
case 'host':
_handleBuildCommand(call?.subCall);
_handleHostCommand(command.subCall);
break;
case 'backend':
_handleBuildCommand(call?.subCall);
_handleBackendCommand(command.subCall);
break;
default:
_askRootCommand();
await _askRootCommand();
break;
}
}
void _askRootCommand() {
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: '')
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
_handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
await _handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
}
void _handleBuildCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
Future<void> _handleBuildCommand(CommandCall? call) async {
if(call == null) {
_askBuildCommand();
return;
}
switch(call.name) {
case 'import':
_handleBuildImportCommand(call!);
await _handleBuildImportCommand(call);
break;
case 'download':
_handleBuildDownloadCommand(call!);
_handleBuildDownloadCommand(call);
break;
case 'list':
_handleBuildListCommand(call);
break;
default:
_askBuildCommand();
@@ -62,23 +102,324 @@ void _handleBuildCommand(CommandCall? call) {
}
}
void _handleBuildImportCommand(CommandCall call) {
final version = call.parameters['path'];
final path = call.parameters['path'];
print(version);
print(path);
void _handleBuildListCommand(CommandCall commandCall) {
List<FortniteVersion> versions;
try {
versions = readVersions();
}catch(error) {
print("$error");
return;
}
void _handleBuildDownloadCommand(CommandCall call) {
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 = [_buildImport.name, _buildDownload.name];
final commands = [_buildList.name, _buildImport.name, _buildDownload.name];
final commandSelector = Select.withTheme(
prompt: ' Select a build command:',
prompt: ' Select a version command:',
options: commands,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '')
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
_handleBuildCommand(CommandCall(name: commands[commandSelector.interact()]));
}
@@ -94,9 +435,3 @@ void _handleHostCommand(CommandCall? call) {
void _handleBackendCommand(CommandCall? call) {
}
void _welcome() => print("""
🎮 Reboot Launcher
🔥 Launch, manage, and play Fortnite using Project Reboot!
🚀 Developed by Auties00 - Version 10.0.7
""".green());

View File

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,9 +1,12 @@
import 'dart:collection';
import 'dart:io';
import 'package:dart_console/dart_console.dart';
class Parser {
typedef AutoComplete = String? Function(String);
class ConsoleParser {
final List<Command> commands;
Parser({required this.commands});
ConsoleParser({required this.commands});
CommandCall? parse(List<String> args) {
var position = 0;
@@ -87,3 +90,71 @@ class CommandCall {
@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

@@ -5,14 +5,15 @@ version: "10.0.7"
publish_to: 'none'
environment:
sdk: ">=2.19.0 <=3.5.3"
sdk: ">=3.0.0 <=3.5.3"
dependencies:
reboot_common:
path: ./../common
tint: ^2.0.1
interact: ^2.2.0
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,6 +23,12 @@ const List<String> kCannotConnectErrors = [
"Network failure when attempting to check platform restrictions",
"UOnlineAccountCommon::ForceLogout"
];
const String kGameFinishedLine = "PlayersLeft: 1";
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,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,6 +35,7 @@ class GameInstance {
}
void _kill() {
if(!killed) {
launched = true;
killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt);
@@ -46,9 +47,4 @@ class GameInstance {
}
}
}
enum GameServerType {
headless,
virtualWindow,
window
}

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,7 +15,13 @@ 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* {
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 {
@@ -137,12 +143,23 @@ Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onEr
window: detached,
);
process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
var killed = false;
process.stdError.listen((error) {
if(!killed) {
log("[BACKEND] Error: $error");
killed = true;
process.kill(ProcessSignal.sigterm);
onError?.call(error);
}
});
if(!detached) {
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
process.exitCode.then((exitCode) {
if(!killed) {
log("[BACKEND] Exit code: $exitCode");
onError?.call("Exit code: $exitCode");
killed = true;
}
});
}
return process;
}
@@ -229,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,464 +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 int _ariaPort = 6800;
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
final String _ariaSecret = "RebootLauncher";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
final List<FortniteBuild> downloadableBuilds = [
FortniteBuild(version: Version.parse("1.7.2"), link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
FortniteBuild(version: Version.parse("1.8"), link: "https://public.simplyblk.xyz/1.8.rar", available: true),
FortniteBuild(version: Version.parse("1.8.1"), link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
FortniteBuild(version: Version.parse("1.8.2"), link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
FortniteBuild(version: Version.parse("1.9"), link: "https://public.simplyblk.xyz/1.9.rar", available: true),
FortniteBuild(version: Version.parse("1.9.1"), link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
FortniteBuild(version: Version.parse("1.10"), link: "https://public.simplyblk.xyz/1.10.rar", available: true),
FortniteBuild(version: Version.parse("1.11"), link: "https://public.simplyblk.xyz/1.11.zip", available: true),
FortniteBuild(version: Version.parse("2.1.0"), link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
FortniteBuild(version: Version.parse("2.2.0"), link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
FortniteBuild(version: Version.parse("2.3"), link: "https://public.simplyblk.xyz/2.3.rar", available: true),
FortniteBuild(version: Version.parse("2.4.0"), link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
FortniteBuild(version: Version.parse("2.4.2"), link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
FortniteBuild(version: Version.parse("2.5.0"), link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
FortniteBuild(version: Version.parse("3.0"), link: "https://public.simplyblk.xyz/3.0.zip", available: true),
FortniteBuild(version: Version.parse("3.1"), link: "https://public.simplyblk.xyz/3.1.rar", available: true),
FortniteBuild(version: Version.parse("3.1.1"), link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
FortniteBuild(version: Version.parse("3.2"), link: "https://public.simplyblk.xyz/3.2.zip", available: true),
FortniteBuild(version: Version.parse("3.3"), link: "https://public.simplyblk.xyz/3.3.rar", available: true),
FortniteBuild(version: Version.parse("3.5"), link: "https://public.simplyblk.xyz/3.5.rar", available: true),
FortniteBuild(version: Version.parse("3.6"), link: "https://public.simplyblk.xyz/3.6.zip", available: true),
FortniteBuild(version: Version.parse("4.0"), link: "https://public.simplyblk.xyz/4.0.zip", available: true),
FortniteBuild(version: Version.parse("4.1"), link: "https://public.simplyblk.xyz/4.1.zip", available: true),
FortniteBuild(version: Version.parse("4.2"), link: "https://public.simplyblk.xyz/4.2.zip", available: true),
FortniteBuild(version: Version.parse("4.4"), link: "https://public.simplyblk.xyz/4.4.rar", available: true),
FortniteBuild(version: Version.parse("4.5"), link: "https://public.simplyblk.xyz/4.5.rar", available: true),
FortniteBuild(version: Version.parse("5.00"), link: "https://public.simplyblk.xyz/5.00.rar", available: true),
FortniteBuild(version: Version.parse("5.0.1"), link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
FortniteBuild(version: Version.parse("5.10"), link: "https://public.simplyblk.xyz/5.10.rar", available: true),
FortniteBuild(version: Version.parse("5.21"), link: "https://public.simplyblk.xyz/5.21.rar", available: true),
FortniteBuild(version: Version.parse("5.30"), link: "https://public.simplyblk.xyz/5.30.rar", available: true),
FortniteBuild(version: Version.parse("5.40"), link: "https://public.simplyblk.xyz/5.40.rar", available: true),
FortniteBuild(version: Version.parse("6.00"), link: "https://public.simplyblk.xyz/6.00.rar", available: true),
FortniteBuild(version: Version.parse("6.01"), link: "https://public.simplyblk.xyz/6.01.rar", available: true),
FortniteBuild(version: Version.parse("6.1.1"), link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
FortniteBuild(version: Version.parse("6.02"), link: "https://public.simplyblk.xyz/6.02.rar", available: true),
FortniteBuild(version: Version.parse("6.2.1"), link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10"), link: "https://public.simplyblk.xyz/6.10.rar", available: true),
FortniteBuild(version: Version.parse("6.10.1"), link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10.2"), link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
FortniteBuild(version: Version.parse("6.21"), link: "https://public.simplyblk.xyz/6.21.rar", available: true),
FortniteBuild(version: Version.parse("6.22"), link: "https://public.simplyblk.xyz/6.22.rar", available: true),
FortniteBuild(version: Version.parse("6.30"), link: "https://public.simplyblk.xyz/6.30.rar", available: true),
FortniteBuild(version: Version.parse("6.31"), link: "https://public.simplyblk.xyz/6.31.rar", available: true),
FortniteBuild(version: Version.parse("7.00"), link: "https://public.simplyblk.xyz/7.00.rar", available: true),
FortniteBuild(version: Version.parse("7.10"), link: "https://public.simplyblk.xyz/7.10.rar", available: true),
FortniteBuild(version: Version.parse("7.20"), link: "https://public.simplyblk.xyz/7.20.rar", available: true),
FortniteBuild(version: Version.parse("7.30"), link: "https://public.simplyblk.xyz/7.30.zip", available: true),
FortniteBuild(version: Version.parse("7.40"), link: "https://public.simplyblk.xyz/7.40.rar", available: true),
FortniteBuild(version: Version.parse("8.00"), link: "https://public.simplyblk.xyz/8.00.zip", available: true),
FortniteBuild(version: Version.parse("8.20"), link: "https://public.simplyblk.xyz/8.20.rar", available: true),
FortniteBuild(version: Version.parse("8.30"), link: "https://public.simplyblk.xyz/8.30.rar", available: true),
FortniteBuild(version: Version.parse("8.40"), link: "https://public.simplyblk.xyz/8.40.zip", available: true),
FortniteBuild(version: Version.parse("8.50"), link: "https://public.simplyblk.xyz/8.50.zip", available: true),
FortniteBuild(version: Version.parse("8.51"), link: "https://public.simplyblk.xyz/8.51.rar", available: true),
FortniteBuild(version: Version.parse("9.00"), link: "https://public.simplyblk.xyz/9.00.zip", available: true),
FortniteBuild(version: Version.parse("9.01"), link: "https://public.simplyblk.xyz/9.01.zip", available: true),
FortniteBuild(version: Version.parse("9.10"), link: "https://public.simplyblk.xyz/9.10.rar", available: true),
FortniteBuild(version: Version.parse("9.21"), link: "https://public.simplyblk.xyz/9.21.zip", available: true),
FortniteBuild(version: Version.parse("9.30"), link: "https://public.simplyblk.xyz/9.30.zip", available: true),
FortniteBuild(version: Version.parse("9.40"), link: "https://public.simplyblk.xyz/9.40.zip", available: true),
FortniteBuild(version: Version.parse("9.41"), link: "https://public.simplyblk.xyz/9.41.rar", available: true),
FortniteBuild(version: Version.parse("10.00"), link: "https://public.simplyblk.xyz/10.00.zip", available: true),
FortniteBuild(version: Version.parse("10.10"), link: "https://public.simplyblk.xyz/10.10.zip", available: true),
FortniteBuild(version: Version.parse("10.20"), link: "https://public.simplyblk.xyz/10.20.zip", available: true),
FortniteBuild(version: Version.parse("10.31"), link: "https://public.simplyblk.xyz/10.31.zip", available: true),
FortniteBuild(version: Version.parse("10.40"), link: "https://public.simplyblk.xyz/10.40.rar", available: true),
FortniteBuild(version: Version.parse("11.00"), link: "https://public.simplyblk.xyz/11.00.zip", available: true),
FortniteBuild(version: Version.parse("11.31"), link: "https://public.simplyblk.xyz/11.31.rar", available: true),
FortniteBuild(version: Version.parse("12.00"), link: "https://public.simplyblk.xyz/12.00.rar", available: true),
FortniteBuild(version: Version.parse("12.21"), link: "https://public.simplyblk.xyz/12.21.zip", available: true),
FortniteBuild(version: Version.parse("12.50"), link: "https://public.simplyblk.xyz/12.50.zip", available: true),
FortniteBuild(version: Version.parse("12.61"), link: "https://public.simplyblk.xyz/12.61.zip", available: true),
FortniteBuild(version: Version.parse("13.00"), link: "https://public.simplyblk.xyz/13.00.rar", available: true),
FortniteBuild(version: Version.parse("13.40"), link: "https://public.simplyblk.xyz/13.40.zip", available: true),
FortniteBuild(version: Version.parse("14.00"), link: "https://public.simplyblk.xyz/14.00.rar", available: true),
FortniteBuild(version: Version.parse("14.40"), link: "https://public.simplyblk.xyz/14.40.rar", available: true),
FortniteBuild(version: Version.parse("14.60"), link: "https://public.simplyblk.xyz/14.60.rar", available: true),
FortniteBuild(version: Version.parse("15.30"), link: "https://public.simplyblk.xyz/15.30.rar", available: true),
FortniteBuild(version: Version.parse("16.40"), link: "https://public.simplyblk.xyz/16.40.rar", available: true),
FortniteBuild(version: Version.parse("17.30"), link: "https://public.simplyblk.xyz/17.30.zip", available: true),
FortniteBuild(version: Version.parse("17.50"), link: "https://public.simplyblk.xyz/17.50.zip", available: true),
FortniteBuild(version: Version.parse("18.40"), link: "https://public.simplyblk.xyz/18.40.zip", available: true),
FortniteBuild(version: Version.parse("19.10"), link: "https://public.simplyblk.xyz/19.10.rar", available: true),
FortniteBuild(version: Version.parse("20.40"), link: "https://public.simplyblk.xyz/20.40.zip", available: true),
];
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
try {
final stopped = _setupLifecycle(options);
await outputFile.parent.create(recursive: true);
final downloadItemCompleter = Completer<File>();
await _startAriaServer();
final downloadId = await _startAriaDownload(options, outputFile);
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.tellStatus",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
}
final result = statusResponseJson["result"];
final files = result["files"] as List?;
if(files == null || files.isEmpty) {
downloadItemCompleter.completeError("Download aborted");
timer.cancel();
return;
}
final error = result["errorCode"];
if(error != null) {
final errorCode = int.tryParse(error);
if(errorCode == 0) {
final path = File(files[0]["path"]);
downloadItemCompleter.complete(path);
}else if(errorCode == 3) {
downloadItemCompleter.completeError("This build is not available yet");
}else {
final errorMessage = result["errorMessage"];
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
}
timer.cancel();
return;
}
final speed = int.parse(result["downloadSpeed"] ?? "0");
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
final totalLength = int.parse(files[0]["length"] ?? "0");
final percentage = completedLength * 100 / totalLength;
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
_onProgress(
options.port,
percentage,
speed,
minutesLeft,
false
);
}catch(error) {
throw "Invalid download status (${error})";
}
});
await Future.any([stopped.future, downloadItemCompleter.future]);
if(!stopped.isCompleted) {
final extension = path.extension(fileName);
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
}else {
await _stopAriaDownload(downloadId);
}
}catch(error) {
_onError(error, options);
}finally {
delete(outputFile);
}
}
Future<void> _startAriaServer() async {
await stopDownloadServer();
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe";
}
final process = await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-secret=$_ariaSecret",
"--rpc-listen-port=$_ariaPort",
"--file-allocation=none",
"--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": [
"token:${_ariaSecret}"
]
};
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return response.statusCode == 200;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
"token:${_ariaSecret}",
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> stopDownloadServer() async {
await killProcessByPort(_ariaPort);
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
throw "Missing 7zip.exe";
}
process = await startProcess(
executable: sevenZip,
args: [
"x",
"-bsp1",
'-o"${options.destination.path}"',
"-y",
'"${tempFile.path}"'
],
);
var completed = false;
process.stdOutput.listen((data) {
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = data.trim().split(" ")[0];
if(!element.endsWith("%")) {
return;
}
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted zip archive", options);
}
});
break;
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
throw "Missing winrar.exe";
}
process = await startProcess(
executable: winrar,
args: [
"x",
"-o+",
'"${tempFile.path}"',
"*.*",
'"${options.destination.path}"'
]
);
var completed = false;
process.stdOutput.listen((data) {
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) {
return;
}
final percentage = int.parse(element).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted rar archive", options);
}
});
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
}
await Future.any([stopped.future, process.exitCode]);
process.kill(ProcessSignal.sigabrt);
}
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
if(percentage == 0) {
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: null,
speed: speed
));
return;
}
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: minutesLeft,
speed: speed
));
}
void _onError(Object? error, FortniteBuildDownloadOptions options) {
if(error != null) {
options.port.send(error.toString());
}
}
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
lifecyclePort.listen((message) {
if(message == kStopBuildDownloadSignal && !stopped.isCompleted) {
stopped.complete();
}
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}

View File

@@ -1,82 +0,0 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
const String kRebootBelowS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
const String kRebootAboveS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
const String _kRebootBelowS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootFallback.zip";
const String _kRebootAboveS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootS20Fallback.zip";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
String? name;
switch(dll) {
case InjectableDll.console:
name = "console.dll";
case InjectableDll.auth:
name = "cobalt.dll";
case InjectableDll.memoryLeak:
name = "memory.dll";
case InjectableDll.gameServer:
name = null;
}
if(name == null) {
return;
}
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}
final output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes, flush: true);
}
Future<void> downloadRebootDll(File file, String url, 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 Exception("status code ${response.statusCode}");
}
}
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

View File

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

View File

@@ -6,3 +6,19 @@ extension ProcessExtension on Process {
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event, allowMalformed: true).split("\n"));
}
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();
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,312 +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);
}
}
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(kDisplayLine) && line.contains(kDisplayInitializedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
onDisplayAttached();
}
}
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;
}
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;
}
}

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

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": "New 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,6 +213,7 @@
"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",
@@ -241,8 +243,8 @@
"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",
@@ -258,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})",
@@ -282,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",
@@ -306,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",
@@ -374,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

@@ -21,8 +21,10 @@ 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;
@@ -161,7 +163,16 @@ class BackendController extends GetxController {
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,
@@ -172,6 +183,7 @@ class BackendController extends GetxController {
)
);
}
}
);
final completer = Completer<bool>();
InfoBarEntry? entry;
@@ -386,7 +398,7 @@ class BackendController extends GetxController {
}
final version = Get.find<GameController>()
.getVersionByName(server.version.toString());
.getVersionByGame(server.version.toString());
if(version == null) {
_showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()),
@@ -508,8 +520,7 @@ class BackendController extends GetxController {
}else {
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>()
.selectedVersion = version;
Get.find<GameController>().selectedVersion.value = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,

View File

@@ -9,11 +9,14 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.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 TextEditingController customGameServerDll;
@@ -27,6 +30,7 @@ class DllController extends GetxController {
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
late final Map<InjectableDll, StreamSubscription?> _subscriptions;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
@@ -39,15 +43,16 @@ class DllController extends GetxController {
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) {
@@ -78,6 +83,7 @@ class DllController extends GetxController {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
@@ -88,6 +94,7 @@ class DllController extends GetxController {
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
@@ -98,14 +105,25 @@ class DllController extends GetxController {
duration: null
);
}
await Future.wait(
final result = await Future.wait(
[
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();
@@ -116,6 +134,7 @@ class DllController extends GetxController {
duration: infoBarShortDuration
);
}
_listenToFileEvents(InjectableDll.gameServer);
return true;
}catch(message) {
infoBarEntry?.close();
@@ -123,26 +142,29 @@ 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;
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.gameServer:
@@ -150,7 +172,7 @@ class DllController extends GetxController {
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);
@@ -163,6 +185,14 @@ class DllController extends GetxController {
}
}
bool _isS20(String version) {
try {
return Version.parse(version).major >= 20;
} on FormatException catch(_) {
return version.trim().startsWith("20.");
}
}
TextEditingController getDllEditingController(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
@@ -198,77 +228,136 @@ class DllController extends GetxController {
}
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 downloadDependency(dll, filePath);
final result = await downloadDependency(dll, filePath);
if(!result) {
entry?.close();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
log("[DLL] Downloaded $dll");
entry?.close();
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(), dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
onDismissed: () => completer.complete(false),
action: Button(
onPressed: () async {
await download(dll, filePath, silent: silent, force: force);
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;
}
}
void guardFiles() {
Future<void> downloadAndGuardDependencies() async {
for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
if(path.equals(controller.text, defaultPath)) {
download(injectable, controller.text);
await download(injectable, controller.text);
}
controller.addListener(() async {
try {
if (!path.equals(controller.text, defaultPath)) {
}
}
void _listenToFileEvents(InjectableDll injectable) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
void onFileEvent(FileSystemEvent event, String filePath) {
if (!path.equals(event.path, filePath)) {
return;
}
final filePath = controller.text;
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (path.equals(event.path, filePath)) {
await download(injectable, filePath);
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);
}
} catch(_) {
// Ignore
}
StreamSubscription subscribe(String filePath) => File(filePath)
.parent
.watch(events: FileSystemEvent.delete | FileSystemEvent.move)
.listen((event) => onFileEvent(event, filePath));
controller.addListener(() {
_subscriptions[injectable]?.cancel();
_subscriptions[injectable] = subscribe(controller.text);
});
_subscriptions[injectable] = subscribe(controller.text);
}
void _updateInput(InjectableDll injectable) {
switch(injectable) {
case InjectableDll.console:
settingsConsoleDllInputKey.currentState?.validate();
break;
case InjectableDll.auth:
settingsAuthDllInputKey.currentState?.validate();
break;
case InjectableDll.gameServer:
settingsGameServerDllInputKey.currentState?.validate();
break;
case InjectableDll.memoryLeak:
settingsMemoryDllInputKey.currentState?.validate();
break;
}
}
}

View File

@@ -1,21 +1,23 @@
import 'dart:async';
import 'dart: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

@@ -13,7 +13,7 @@ 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

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

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

@@ -47,448 +47,6 @@ 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();
}

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

@@ -13,10 +13,14 @@ 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

@@ -7,6 +7,7 @@ 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';
@@ -14,9 +15,7 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/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';
@@ -45,7 +44,6 @@ class _LaunchButtonState extends State<LaunchButton> {
InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation;
Completer? _pingOperation;
IVirtualDesktop? _virtualDesktop;
@override
Widget build(BuildContext context) => Align(
@@ -73,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;
}
@@ -93,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.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();
}
@@ -139,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");
@@ -174,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");
@@ -184,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;
}
@@ -208,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;
@@ -228,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){
@@ -246,22 +242,60 @@ class _LaunchButtonState extends State<LaunchButton> {
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,
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"
}
@@ -272,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,
@@ -301,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(
@@ -397,7 +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();
@@ -412,6 +425,14 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
bool _isChapterOne(String version) {
try {
return Version.parse(version).major < 10;
} on FormatException catch(_) {
return true;
}
}
void _onGameClientInjected() {
_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) {
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
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,52 +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: 1)
const Duration(seconds: 10)
);
this._pingOperation = pingOperation;
_pingOperation = pingOperation;
publicPingResult = await pingOperation.future;
_gameServerInfoBar?.close();
_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;
_gameServerInfoBar?.close();
return result;
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
// Ignore: might have been already terminated, don't bother checking
} finally {
_pingOperation = null;
}
await _operation?.cancel();
_operation = null;
_backendController.stop(interactive: false);
}
host = host ?? widget.host;
await _operation?.cancel();
_operation = null;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){
@@ -538,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) {
@@ -562,11 +611,16 @@ 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);
if(interactive) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) {
_gameServerInfoBar?.close();
@@ -594,18 +648,27 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration,
);
break;
case _StopReason.multipleExecutablesError:
showRebootInfoBar(
translations.multipleExecutablesError(error ?? translations.unknown),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.exitCode:
if(instance != null && !instance.launched) {
final injectedDlls = instance.injectedDlls;
showRebootInfoBar(
translations.corruptedVersionError,
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,
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
@@ -630,8 +693,9 @@ class _LaunchButtonState extends State<LaunchButton> {
break;
case _StopReason.tokenError:
_backendController.stop(interactive: false);
final injectedDlls = instance?.injectedDlls;
showRebootInfoBar(
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
@@ -642,7 +706,7 @@ class _LaunchButtonState extends State<LaunchButton> {
break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? "game server" : "client"),
translations.fortniteCrashError(host ? translations.gameServer : translations.client),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
@@ -654,6 +718,14 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration,
);
break;
case _StopReason.gameServerPortError:
showRebootInfoBar(
translations.gameServerPortEqualsBackendPort(kDefaultBackendPort),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
}
}
}
@@ -671,15 +743,11 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
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}");
@@ -694,13 +762,16 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
Future<File?> _getDllFileOrStop(String version, InjectableDll injectable, bool host) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
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");
@@ -709,14 +780,20 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: host
);
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _dllController.download(injectable, 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(
@@ -725,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,
@@ -743,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),
@@ -760,6 +837,7 @@ enum _StopReason {
normal,
missingVersionError,
missingExecutableError,
multipleExecutablesError,
corruptedVersionError,
missingCustomDllError,
corruptedDllError,
@@ -767,6 +845,7 @@ enum _StopReason {
matchmakerError,
tokenError,
unknownError,
gameServerPortError,
exitCode,
crash;

View File

@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/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

@@ -180,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;

View File

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

View File

@@ -5,12 +5,10 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package: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/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/data.dart';
@@ -19,7 +17,7 @@ import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/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_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
@@ -78,7 +76,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
@override
List<Widget> get settings => [
_information,
buildVersionSelector(
VersionSelector.buildTile(
key: hostVersionOverlayTargetKey
),
_options,
@@ -133,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(
@@ -214,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

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

View File

@@ -17,6 +17,11 @@ import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package: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);
@@ -60,33 +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.download(InjectableDll.console, 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: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
_dllController.backendDll.text = path;
_dllController.download(InjectableDll.auth, 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: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
_dllController.memoryLeakDll.text = path;
_dllController.download(InjectableDll.memoryLeak, path, force: true);
await _dllController.download(InjectableDll.memoryLeak, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
),
_internalFilesServerType,
@@ -181,13 +192,15 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
);
}else {
return createFileSetting(
key: settingsGameServerDllInputKey,
title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.customGameServerDll,
onReset: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
_dllController.customGameServerDll.text = path;
_dllController.download(InjectableDll.gameServer, path);
await _dllController.download(InjectableDll.gameServer, path);
settingsGameServerDllInputKey.currentState?.validate();
}
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
name: reboot_launcher
description: Graphical User Interface for Project Reboot
version: "10.0.7"
version: "10.0.9"
publish_to: 'none'
@@ -33,9 +33,6 @@ dependencies:
# Window management
window_manager: ^0.4.2
# Extract zip archives (for example the reboot.zip)
archive: ^3.6.1
# Cryptographic functions
bcrypt: ^1.1.3
pointycastle: ^3.9.1
@@ -49,12 +46,15 @@ dependencies:
get: ^4.6.6
# Native utilities
archive: ^3.6.1
watcher: ^1.1.1
clipboard: ^0.1.3
app_links: ^6.3.2
windows_taskbar: ^1.1.2
file_picker: ^8.1.2
url_launcher: ^6.3.0
local_notifier: ^0.1.6
port_forwarder: ^1.0.0
# Server browser
supabase_flutter: ^2.7.0

View File

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