43 Commits

Author SHA1 Message Date
Alessandro Autiero
9c6cd6dd37 Merge remote-tracking branch 'origin/master' 2025-04-16 15:43:43 +02:00
Alessandro Autiero
c3ede3b745 10.0.9 2025-04-16 15:43:34 +02:00
Alessandro Autiero
d2f0d176eb Update PortForwarding.md 2025-04-08 18:36:09 +02:00
Alessandro Autiero
f9cf99a6b2 Update README.md 2025-03-24 20:50:43 +01:00
Alessandro Autiero
dc2d4c4377 10.0.8 2025-03-23 23:17:20 +01:00
Alessandro Autiero
5d8f6bf0fa 10.0.8 2025-03-23 20:26:13 +01:00
Alessandro Autiero
9a000db3b7 10.0.8 2025-03-23 18:25:47 +01:00
Alessandro Autiero
4327541ac6 Merge pull request #257 from Milxnor/master
Added dedicated_server endpoints & Update keychain
2025-03-23 16:33:39 +01:00
Gray
64dc971da4 Added dedicated_server endpoints 2025-03-22 07:47:46 -04:00
Gray
d36da909ed Update keychain (for events and new cosmetics) 2025-03-22 07:47:25 -04:00
Alessandro Autiero
90448eeaa1 10.0.7 2025-03-08 17:06:01 +01:00
Alessandro Autiero
b319479def 10.0.6 2025-02-04 13:50:01 +01:00
Alessandro Autiero
d5e41ed646 10.0.5 2024-12-30 19:13:08 +01:00
Alessandro Autiero
9e20ec86e6 Merge remote-tracking branch 'origin/master' 2024-12-29 21:43:04 +01:00
Alessandro Autiero
004fc41292 Dependency 2024-12-29 21:42:54 +01:00
Alessandro Autiero
ee466df630 Update README.md 2024-12-24 21:52:06 +01:00
Alessandro Autiero
fdb1d694d9 Better moving system 2024-12-10 17:18:10 +01:00
Alessandro Autiero
0cfa4af236 10.0.4 2024-12-10 14:45:56 +01:00
Alessandro Autiero
d42946c44b 10.0.3 2024-12-09 22:28:24 +01:00
Alessandro Autiero
0a59a32c1b 10.0.2 2024-12-09 14:36:43 +01:00
Alessandro Autiero
2046cb14f6 10.0 2024-12-09 12:59:14 +01:00
Alessandro Autiero
e3f7a1d2cc 10.0 2024-12-09 12:49:21 +01:00
Alessandro Autiero
cd6752ed3f Merge pull request #169 from Auties00/_onLoggedIn
10.0
2024-12-09 12:44:14 +01:00
Alessandro Autiero
e1df46efd9 10.0 2024-12-09 12:42:49 +01:00
Alessandro Autiero
dccd05e57f Merge pull request #168 from Auties00/_onLoggedIn
10.0
2024-12-09 12:15:20 +01:00
Alessandro Autiero
eb7745cc4d 10.0 2024-12-09 12:14:41 +01:00
Alessandro Autiero
7d5e17642a Merge pull request #166 from Auties00/_onLoggedIn
Switched to starfall.dll
2024-12-08 20:42:54 +01:00
Alessandro Autiero
6f91ad0404 Switched to starfall.dll 2024-12-08 20:41:31 +01:00
Alessandro Autiero
0c38528e77 Merge pull request #118 from Auties00/_onLoggedIn
On logged in
2024-10-21 22:34:47 +02:00
Alessandro Autiero
dfebe74518 Switched to sinum 2024-10-21 20:32:23 +02:00
Alessandro Autiero
bfe15e43d9 Released 9.2.7 2024-09-14 12:37:56 +02:00
Alessandro Autiero
62dae468bf Merge pull request #98 from Auties00/_onLoggedIn
Released 9.2.6
2024-09-12 17:49:12 +02:00
Alessandro Autiero
a9af28273a Released 9.2.6 2024-09-12 15:46:24 +02:00
Alessandro Autiero
232bf8fbfc Update README.md 2024-08-18 22:35:17 +02:00
Alessandro Autiero
a787c4efc9 Merge pull request #86 from Auties00/_onLoggedIn
Release 9.2.5
2024-08-18 22:34:36 +02:00
Alessandro Autiero
4c3fe9bc65 Released 9.2.5 2024-08-18 20:29:09 +02:00
Alessandro Autiero
3f88d5ed80 Create .gitattributes 2024-07-31 11:54:02 +02:00
Alessandro Autiero
582270849e Released 9.2.4 2024-07-10 15:40:52 +02:00
Alessandro Autiero
1ef4e76768 Small fix to display errors and warnings from backend 2024-07-10 15:19:20 +02:00
Alessandro Autiero
cd8c8e6dd9 Release 9.2.3 2024-07-10 15:11:49 +02:00
Alessandro Autiero
170a878e79 Merge pull request #69 from Auties00/_onLoggedIn
Release 9.2.2
2024-07-09 22:38:47 +02:00
Alessandro Autiero
a2505011d9 Release 9.2.2 2024-07-09 20:38:01 +02:00
Alessandro Autiero
3e2c2e96b1 Release 9.2.1 2024-07-07 10:17:07 +02:00
129 changed files with 9681 additions and 6993 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
backend/**/* linguist-vendored

View File

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

2
backend/index.js vendored
View File

@@ -35,7 +35,7 @@ express.use(require("./structure/matchmaking.js"));
express.use(require("./structure/cloudstorage.js")); express.use(require("./structure/cloudstorage.js"));
express.use(require("./structure/mcp.js")); express.use(require("./structure/mcp.js"));
const port = process.env.PORT || 3551; const port = 3551;
express.listen(port, () => { express.listen(port, () => {
console.log("LawinServer started listening on port", port); console.log("LawinServer started listening on port", port);

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,87 +0,0 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:reboot_cli/src/game.dart';
import 'package:reboot_cli/src/reboot.dart';
import 'package:reboot_cli/src/server.dart';
import 'package:reboot_common/common.dart';
late String? username;
late bool host;
late bool verbose;
late String dll;
late FortniteVersion version;
late bool autoRestart;
void main(List<String> args) async {
stdout.writeln("Reboot Launcher");
stdout.writeln("Wrote by Auties00");
stdout.writeln("Version 1.0");
kill();
var parser = ArgParser()
..addOption("path", mandatory: true)
..addOption("username")
..addOption("server-type", allowed: ServerType.values.map((entry) => entry.name), defaultsTo: ServerType.embedded.name)
..addOption("server-host")
..addOption("server-port")
..addOption("matchmaking-address")
..addOption("dll", defaultsTo: rebootDllFile.path)
..addFlag("update", defaultsTo: true, negatable: true)
..addFlag("log", defaultsTo: false)
..addFlag("host", defaultsTo: false)
..addFlag("auto-restart", defaultsTo: false, negatable: true);
var result = parser.parse(args);
dll = result["dll"];
host = result["host"];
username = result["username"] ?? kDefaultPlayerName;
verbose = result["log"];
version = FortniteVersion(name: "Dummy", location: Directory(result["path"]));
await downloadRequiredDLLs();
if(result["update"]) {
stdout.writeln("Updating reboot dll...");
try {
await downloadRebootDll(kRebootDownloadUrl);
}catch(error){
stderr.writeln("Cannot update reboot dll: $error");
}
}
stdout.writeln("Launching game...");
var executable = version.shippingExecutable;
if(executable == null){
throw Exception("Missing game executable at: ${version.location.path}");
}
final serverHost = result["server-host"]?.trim();
if(serverHost?.isEmpty == true){
throw Exception("Missing host name");
}
final serverPort = result["server-port"]?.trim();
if(serverPort?.isEmpty == true){
throw Exception("Missing port");
}
final serverPortNumber = serverPort == null ? null : int.tryParse(serverPort);
if(serverPort != null && serverPortNumber == null){
throw Exception("Invalid port, use only numbers");
}
var started = await startServerCli(
serverHost,
serverPortNumber,
ServerType.values.firstWhere((element) => element.name == result["server-type"])
);
if(!started){
stderr.writeln("Cannot start server!");
return;
}
writeMatchmakingIp(result["matchmaking-address"]);
autoRestart = result["auto-restart"];
await startGame();
}

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

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

View File

View File

View File

View File

View File

View File

View File

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

View File

@@ -1,123 +0,0 @@
import 'dart:io';
import 'package:process_run/process_run.dart';
import 'package:reboot_cli/cli.dart';
import 'package:reboot_common/common.dart';
Process? _gameProcess;
Process? _launcherProcess;
Process? _eacProcess;
Future<void> startGame() async {
await _startLauncherProcess(version);
await _startEacProcess(version);
var executable = await version.shippingExecutable;
if (executable == null) {
throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?");
}
if (username == null) {
username = "Reboot${host ? 'Host' : 'Player'}";
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
}
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
..exitCode.then((_) => _onClose())
..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
_injectOrShowError("cobalt.dll");
}
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.launcherExecutable == null) {
return;
}
_launcherProcess = await Process.start(dummyVersion.launcherExecutable!.path, []);
suspend(_launcherProcess!.pid);
}
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.eacExecutable == null) {
return;
}
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
suspend(_eacProcess!.pid);
}
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
if(verbose) {
stdout.writeln(line);
}
handleGameOutput(
line: line,
host: hosting,
onDisplayAttached: () {}, // TODO: Support virtual desktops
onLoggedIn: onLoggedIn,
onMatchEnd: onMatchEnd,
onShutdown: onShutdown,
onTokenError: onTokenError,
onBuildCorrupted: onBuildCorrupted
);
if (line.contains(kShutdownLine)) {
_onClose();
return;
}
if(kCannotConnectErrors.any((element) => line.contains(element))){
stderr.writeln("The backend doesn't work! Token expired");
_onClose();
return;
}
if(line.contains("Region ")){
if(hosting) {
_injectOrShowError(dll, false);
}else {
_injectOrShowError("console.dll");
}
_injectOrShowError("memory.dll");
}
}
void _kill() {
_gameProcess?.kill(ProcessSignal.sigabrt);
_launcherProcess?.kill(ProcessSignal.sigabrt);
_eacProcess?.kill(ProcessSignal.sigabrt);
}
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
if (_gameProcess == null) {
return;
}
try {
stdout.writeln("Injecting $binary...");
var dll = locate ? File("${dllsDirectory.path}\\$binary") : File(binary);
if(!dll.existsSync()){
throw Exception("Cannot inject $dll: missing file");
}
await injectDll(_gameProcess!.pid, dll);
} catch (exception) {
throw Exception("Cannot inject binary: $binary");
}
}
void _onClose() {
_kill();
sleep(const Duration(seconds: 3));
stdout.writeln("The game was closed");
if(autoRestart){
stdout.writeln("Restarting automatically game");
startGame();
return;
}
exit(0);
}

View File

@@ -1,55 +0,0 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:reboot_common/common.dart';
// TODO: Use github
const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memory.dll";
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
Future<void> downloadRequiredDLLs() async {
stdout.writeln("Downloading necessary components...");
var consoleDll = File("${dllsDirectory.path}\\console.dll");
if(!consoleDll.existsSync()){
var response = await http.get(Uri.parse(_consoleDownload));
if(response.statusCode != 200){
throw Exception("Cannot download console.dll");
}
await consoleDll.writeAsBytes(response.bodyBytes);
}
var craniumDll = File("${dllsDirectory.path}\\cobalt.dll");
if(!craniumDll.existsSync()){
var response = await http.get(Uri.parse(_baseDownload));
if(response.statusCode != 200){
throw Exception("Cannot download cobalt.dll");
}
await craniumDll.writeAsBytes(response.bodyBytes);
}
var memoryFixDll = File("${dllsDirectory.path}\\memory.dll");
if(!memoryFixDll.existsSync()){
var response = await http.get(Uri.parse(_memoryFixDownload));
if(response.statusCode != 200){
throw Exception("Cannot download memory.dll");
}
await memoryFixDll.writeAsBytes(response.bodyBytes);
}
if(!backendDirectory.existsSync()){
var response = await http.get(Uri.parse(_embeddedConfigDownload));
if(response.statusCode != 200){
throw Exception("Cannot download embedded server config");
}
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, backendDirectory.path);
}
}

View File

@@ -1,60 +0,0 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/util/backend.dart' as server;
Future<bool> startServerCli(String? host, int? port, ServerType type) async {
stdout.writeln("Starting backend server...");
switch(type){
case ServerType.local:
var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
if(result == null){
throw Exception("Local backend server is not running");
}
stdout.writeln("Detected local backend server");
return true;
case ServerType.embedded:
stdout.writeln("Starting an embedded server...");
await server.startEmbeddedBackend(false);
var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
if(result == null){
throw Exception("Cannot start embedded server");
}
return true;
case ServerType.remote:
if(host == null){
throw Exception("Missing host for remote server");
}
if(port == null){
throw Exception("Missing host for remote server");
}
stdout.writeln("Starting a reverse proxy to $host:$port");
return await _changeReverseProxyState(host, port) != null;
}
}
Future<HttpServer?> _changeReverseProxyState(String host, int port) async {
try{
var uri = await pingBackend(host, port);
if(uri == null){
return null;
}
return await server.startRemoteBackendProxy(uri);
}catch(error){
throw Exception("Cannot start reverse proxy");
}
}
void kill() async {
try {
await Process.run("taskkill", ["/f", "/im", "FortniteLauncher.exe"]);
await Process.run("taskkill", ["/f", "/im", "FortniteClient-Win64-Shipping_EAC.exe"]);
}catch(_){
}
}

View File

@@ -0,0 +1,160 @@
import 'dart:io';
import 'package:dart_console/dart_console.dart';
typedef AutoComplete = String? Function(String);
class ConsoleParser {
final List<Command> commands;
ConsoleParser({required this.commands});
CommandCall? parse(List<String> args) {
var position = 0;
var allowedCommands = _toMap(commands);
var allowedParameters = <String>{};
Command? command;
CommandCall? head;
CommandCall? tail;
String? parameterName;
while(position < args.length) {
final current = args[position].toLowerCase();
if(parameterName != null) {
tail?.parameters[parameterName] = current;
parameterName = null;
}else if(allowedParameters.contains(current.toLowerCase())) {
parameterName = current.substring(2);
if(args.elementAtOrNull(position + 1) == '"') {
position++;
}
}else {
final newCommand = allowedCommands[current];
if(newCommand != null) {
final newCall = CommandCall(name: newCommand.name);
if(head == null) {
head = newCall;
tail = newCall;
}
if(tail != null) {
tail.subCall = newCall;
}
tail = newCall;
command = newCommand;
allowedCommands = _toMap(newCommand.subCommands);
allowedParameters = _toParameters(command);
}
}
position++;
}
return head;
}
Set<String> _toParameters(Command? parent) => parent?.parameters
.map((e) => '--${e.toLowerCase()}')
.toSet() ?? {};
Map<String, Command> _toMap(List<Command> children) => Map.fromIterable(
children,
key: (command) => command.name.toLowerCase(),
value: (command) => command
);
}
class Command {
final String name;
final List<String> parameters;
final List<Command> subCommands;
const Command({required this.name, required this.parameters, required this.subCommands});
@override
String toString() => 'Command{name: $name, parameters: $parameters, subCommands: $subCommands}';
}
class Parameter {
final String name;
final bool Function(String) validator;
const Parameter({required this.name, required this.validator});
@override
String toString() => 'Parameter{name: $name, validator: $validator}';
}
class CommandCall {
final String name;
final Map<String, String> parameters;
CommandCall? subCall;
CommandCall({required this.name}) : parameters = {};
@override
String toString() => 'CommandCall{name: $name, parameters: $parameters, subCall: $subCall}';
}
String runAutoComplete(AutoComplete completion) {
final console = Console();
console.rawMode = true;
final position = console.cursorPosition!;
var currentInput = '';
var running = true;
var result = '';
while (running) {
final key = console.readKey();
switch (key.controlChar) {
case ControlCharacter.ctrlC:
running = false;
break;
case ControlCharacter.enter:
_eraseUntil(console, position);
console.write(currentInput);
console.writeLine();
result = currentInput;
running = false;
break;
case ControlCharacter.tab:
final suggestion = completion(currentInput);
if (suggestion != null) {
_eraseUntil(console, position);
currentInput = suggestion;
console.write(currentInput);
}
break;
case ControlCharacter.backspace:
if (currentInput.isNotEmpty) {
currentInput = currentInput.substring(0, currentInput.length - 1);
_eraseUntil(console, position);
console.write(currentInput);
_showSuggestion(console, position, currentInput, completion);
}
break;
default:
currentInput += key.char;
console.write(key.char);
_showSuggestion(console, position, currentInput, completion);
}
}
return result;
}
void _eraseUntil(Console console, Coordinate position) {
console.cursorPosition = position;
stdout.write('\x1b[K');
}
void _showSuggestion(Console console, Coordinate position, String input, AutoComplete completion) {
final suggestion = completion(input);
if(suggestion == null) {
_eraseUntil(console, position);
console.write(input);
}else if(suggestion.length > input.length) {
final remaining = suggestion.substring(input.length);
final cursorPosition = console.cursorPosition;
console.setForegroundColor(ConsoleColor.brightBlack);
console.write(remaining);
console.resetColorAttributes();
console.cursorPosition = cursorPosition;
}
}

View File

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

View File

@@ -1,17 +1,19 @@
name: reboot_cli name: reboot_cli
description: Command Line Interface for Project Reboot description: Command Line Interface for Project Reboot
version: "1.0.0" version: "10.0.7"
publish_to: 'none' publish_to: 'none'
environment: environment:
sdk: ">=2.19.0 <=3.3.4" sdk: ">=3.0.0 <=3.5.3"
dependencies: dependencies:
reboot_common: reboot_common:
path: ./../common path: ./../common
args: ^2.3.1 tint: ^2.0.1
process_run: ^0.13.1 interact_cli: ^2.4.0
args: ^2.6.0
version: ^3.0.2
dependency_overrides: dependency_overrides:
xml: ^6.3.0 xml: ^6.3.0

View File

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

View File

@@ -1,4 +1,7 @@
import 'package:version/version.dart';
const String kDefaultPlayerName = "Player"; const String kDefaultPlayerName = "Player";
const String kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1"; const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777"; const String kDefaultGameServerPort = "7777";
const String kInitializedLine = "Game Engine Initialized"; const String kInitializedLine = "Game Engine Initialized";
@@ -11,7 +14,7 @@ const List<String> kCorruptedBuildErrors = [
"Critical error", "Critical error",
"when 0 bytes remain", "when 0 bytes remain",
"Pak chunk signature verification failed!", "Pak chunk signature verification failed!",
"Couldn't find pak signature file" "LogWindows:Error: Fatal error!"
]; ];
const List<String> kCannotConnectErrors = [ const List<String> kCannotConnectErrors = [
"port 3551 failed: Connection refused", "port 3551 failed: Connection refused",
@@ -20,5 +23,12 @@ const List<String> kCannotConnectErrors = [
"Network failure when attempting to check platform restrictions", "Network failure when attempting to check platform restrictions",
"UOnlineAccountCommon::ForceLogout" "UOnlineAccountCommon::ForceLogout"
]; ];
const String kGameFinishedLine = "PlayersLeft: 1"; const String kGameFinishedLine = "TeamsLeft: 1";
const String kDisplayInitializedLine = "Display"; const String kDisplayLine = "Display";
const String kDisplayInitializedLine = "Initialized";
const String kShippingExe = "FortniteClient-Win64-Shipping.exe";
const String kLauncherExe = "FortniteLauncher.exe";
const String kEacExe = "FortniteClient-Win64-Shipping_EAC.exe";
const String kCrashReportExe = "CrashReportClient.exe";
const String kGFSDKAftermathLibDll = "GFSDK_Aftermath_Lib.dll";
final Version kMaxAllowedVersion = Version.parse("30.10");

View File

@@ -1,41 +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{
final result = directory.listSync(recursive: true)
.firstWhere((element) => path.basename(element.path) == name);
return File(result.path);
}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,6 +1,10 @@
enum InjectableDll { enum InjectableDll {
console, console,
cobalt, auth,
reboot, gameServer,
memory memoryLeak
}
extension InjectableDllVersionAware on InjectableDll {
bool get isVersionDependent => this == InjectableDll.gameServer;
} }

View File

@@ -1,15 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:version/version.dart';
class FortniteBuild { class FortniteBuild {
final Version version; final String gameVersion;
final String link; final String link;
final bool available; final bool available;
FortniteBuild({ FortniteBuild({
required this.version, required this.gameVersion,
required this.link, required this.link,
required this.available required this.available
}); });
@@ -17,13 +15,15 @@ class FortniteBuild {
class FortniteBuildDownloadProgress { class FortniteBuildDownloadProgress {
final double progress; final double progress;
final int? minutesLeft; final int? timeLeft;
final bool extracting; final bool extracting;
final int speed;
FortniteBuildDownloadProgress({ FortniteBuildDownloadProgress({
required this.progress, required this.progress,
required this.extracting, required this.extracting,
this.minutesLeft, required this.timeLeft,
required this.speed
}); });
} }

View File

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

View File

@@ -4,26 +4,27 @@ import 'package:reboot_common/common.dart';
class GameInstance { class GameInstance {
final String versionName; final String version;
final bool host;
final int gamePid; final int gamePid;
final int? launcherPid; final int? launcherPid;
final int? eacPid; final int? eacPid;
final List<InjectableDll> injectedDlls; final List<InjectableDll> injectedDlls;
final GameServerType? serverType; final bool headless;
bool launched; bool launched;
bool movedToVirtualDesktop;
bool tokenError; bool tokenError;
bool killed; bool killed;
GameInstance? child; GameInstance? child;
GameInstance({ GameInstance({
required this.versionName, required this.version,
required this.host,
required this.gamePid, required this.gamePid,
required this.launcherPid, required this.launcherPid,
required this.eacPid, required this.eacPid,
required this.serverType, required this.headless,
required this.child required this.child
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = []; }): tokenError = false, killed = false, launched = false, injectedDlls = [];
void kill() { void kill() {
GameInstance? child = this; GameInstance? child = this;
@@ -34,6 +35,7 @@ class GameInstance {
} }
void _kill() { void _kill() {
if(!killed) {
launched = true; launched = true;
killed = true; killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt); Process.killPid(gamePid, ProcessSignal.sigabrt);
@@ -45,9 +47,4 @@ class GameInstance {
} }
} }
} }
enum GameServerType {
headless,
virtualWindow,
window
} }

View File

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

View File

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

View File

@@ -1,294 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart';
import 'package:version/version.dart';
const String kStopBuildDownloadSignal = "kill";
final Dio _dio = _buildDioInstance();
Dio _buildDioInstance() {
final dio = Dio();
final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter;
httpClientAdapter.createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
return client;
};
return dio;
}
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
const String _unavailableError = "The build downloader is not available right now";
const String _genericError = "The build downloader is not working correctly";
const int _maxErrors = 100;
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
final response = await _dio.get<String>(
_archiveSourceUrl,
options: Options(
responseType: ResponseType.plain
)
);
if (response.statusCode != 200) {
return [];
}
var results = <FortniteBuild>[];
for (final line in response.data?.split("\n") ?? []) {
if (!line.startsWith("|")) {
continue;
}
var parts = line.substring(1, line.length - 1).split("|");
if (parts.isEmpty) {
continue;
}
var versionName = parts.first.trim();
final separator = versionName.indexOf("-");
if(separator != -1) {
versionName = versionName.substring(0, separator);
}
final link = parts.last.trim();
try {
results.add(FortniteBuild(
version: Version.parse(versionName),
link: link,
available: link.endsWith(".zip") || link.endsWith(".rar")
));
} on FormatException {
// Ignore
}
}
return results;
}
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
try {
final stopped = _setupLifecycle(options);
final outputDir = Directory("${options.destination.path}\\.build");
await outputDir.create(recursive: true);
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final extension = path.extension(fileName);
final tempFile = File("${outputDir.path}\\$fileName");
if(await tempFile.exists()) {
await tempFile.delete(recursive: true);
}
final startTime = DateTime.now().millisecondsSinceEpoch;
final response = _downloadArchive(options, stopped, tempFile, startTime);
await Future.any([stopped.future, response]);
if(!stopped.isCompleted) {
await _extractArchive(stopped, extension, tempFile, options);
}
delete(outputDir);
}catch(error) {
_onError(error, options);
}
}
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, Completer stopped, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
var received = byteStart ?? 0;
try {
await _dio.download(
options.build.link,
tempFile.path,
onReceiveProgress: (data, length) {
if(stopped.isCompleted) {
throw StateError("Download interrupted");
}
received = data;
final percentage = (received / length) * 100;
_onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options);
},
deleteOnError: false,
options: Options(
validateStatus: (statusCode) {
if(statusCode == 200) {
return true;
}
if(statusCode == 403 || statusCode == 503) {
throw _deniedConnectionError;
}
if(statusCode == 404) {
throw _unavailableError;
}
throw _genericError;
},
headers: byteStart == null || byteStart <= 0 ? {
"Cookie": "_c_t_c=1"
} : {
"Cookie": "_c_t_c=1",
"Range": "bytes=${byteStart}-"
},
)
);
}catch(error) {
if(stopped.isCompleted) {
return;
}
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) {
_onError(error, options);
return;
}
await _downloadArchive(options, stopped, tempFile, startTime, received, errorsCount + 1);
}
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
final startTime = DateTime.now().millisecondsSinceEpoch;
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
throw "Corrupted installation: 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) {
final now = DateTime.now().millisecondsSinceEpoch;
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(startTime, now, 100, true, options);
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(startTime, now, percentage, true, options);
});
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 "Corrupted installation: missing winrar.exe";
}
process = await startProcess(
executable: winrar,
args: [
"x",
"-o+",
'"${tempFile.path}"',
"*.*",
'"${options.destination.path}"'
]
);
var completed = false;
process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch;
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) {
return;
}
final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options);
});
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(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
if(percentage == 0) {
options.port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting
));
return;
}
final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now;
final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round();
options.port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
minutesLeft: minutesLeft
));
}
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,71 +0,0 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
bool _watcher = false;
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
const String kRebootDownloadUrl =
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootDllFile.exists();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
Future<void> downloadCriticalDll(String name, String outputPath) async {
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}
final output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes, flush: true);
}
Future<int> downloadRebootDll(String url) async {
Directory? outputDir;
final now = DateTime.now();
try {
final response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
throw Exception("Cannot download reboot.zip: status code ${response.statusCode}");
}
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
return now.millisecondsSinceEpoch;
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}
Stream<String> watchDlls() async* {
if(_watcher) {
return;
}
_watcher = true;
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (event.path.endsWith(".dll")) {
yield event.path;
}
}
}

View File

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

View File

@@ -6,3 +6,19 @@ extension ProcessExtension on Process {
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event, allowMalformed: true).split("\n")); 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,13 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart'; import 'package:synchronized/extension.dart';
final File launcherLogFile = _createLoggingFile(); final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1); bool enableLoggingToConsole = true;
File _createLoggingFile() { File _createLoggingFile() {
final file = File("${logsDirectory.path}\\launcher.log"); final file = File("${installationDirectory.path}\\launcher.log");
file.parent.createSync(recursive: true); file.parent.createSync(recursive: true);
if(file.existsSync()) { if(file.existsSync()) {
file.deleteSync(); file.deleteSync();
@@ -18,12 +18,14 @@ File _createLoggingFile() {
void log(String message) async { void log(String message) async {
try { try {
await _semaphore.acquire(); if(enableLoggingToConsole) {
print(message); print(message);
}
launcherLogFile.synchronized(() async {
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true); await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
});
}catch(error) { }catch(error) {
print("[LOGGER_ERROR] An error occurred while logging: $error"); print("[LOGGER_ERROR] An error occurred while logging: $error");
}finally {
_semaphore.release();
} }
} }

View File

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

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

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

View File

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

View File

@@ -1,40 +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 logsDirectory =>
Directory("${installationDirectory.path}\\logs");
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,332 +0,0 @@
// ignore_for_file: non_constant_identifier_names
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.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 ? SW_SHOWNORMAL : SW_HIDE;
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
var shellResult = ShellExecuteEx(shellInput);
return shellResult == 1;
}
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
final argsOrEmpty = args ?? [];
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: executable.parent.path,
environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _withLogger(name, executable, process, window);
}
final process = await Process.start(
executable.path,
args ?? [],
workingDirectory: executable.parent.path,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _withLogger(name, executable, process, window);
}
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) {
final extendedProcess = _ExtendedProcess(process, true);
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log");
loggingFile.parent.createSync(recursive: true);
if(loggingFile.existsSync()) {
loggingFile.deleteSync();
}
final semaphore = Semaphore(1);
void logEvent(String event) async {
await semaphore.acquire();
await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true);
semaphore.release();
}
extendedProcess.stdOutput.listen(logEvent);
extendedProcess.stdError.listen(logEvent);
if(!window) {
extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n"));
}
return extendedProcess;
}
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_SUSPEND_RESUME, FALSE, pid);
final result = _NtSuspendProcess(processHandle);
CloseHandle(processHandle);
return result == 0;
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = _NtResumeProcess(processHandle);
CloseHandle(processHandle);
return result == 0;
}
void _watchProcess(int pid) {
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
try {
WaitForSingleObject(processHandle, INFINITE);
}finally {
CloseHandle(processHandle);
}
}
Future<bool> watchProcess(int pid) async {
var completer = Completer<bool>();
var exitPort = ReceivePort();
exitPort.listen((_) {
if(!completer.isCompleted) {
completer.complete(true);
}
});
var errorPort = ReceivePort();
errorPort.listen((_) => completer.complete(false));
await Isolate.spawn(
_watchProcess,
pid,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
errorsAreFatal: true
);
return await completer.future;
}
// TODO: Template
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) {
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
final args = [
"-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(log) {
args.add("-log");
}
if(host) {
args.addAll([
"-nosplash",
"-nosound"
]);
if(hostType == GameServerType.headless){
args.add("-nullrhi");
}
}
if(additionalArgs.isNotEmpty){
args.addAll(additionalArgs.split(" "));
}
return args;
}
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)) {
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
onMatchEnd();
}else if(line.contains(kDisplayInitializedLine) && host) {
onDisplayAttached();
}
}
String _parseUsername(String username, bool host) {
if(host) {
return "Player${Random().nextInt(1000)}";
}
if (username.isEmpty) {
return kDefaultPlayerName;
}
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}
final class _ExtendedProcess implements Process {
final Process _delegate;
final Stream<List<int>>? _stdout;
final Stream<List<int>>? _stderr;
_ExtendedProcess(Process delegate, bool attached) :
_delegate = delegate,
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
@override
Future<int> get exitCode => _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

@@ -7,19 +7,19 @@ environment:
sdk: ">=3.0.0 <=4.0.0" sdk: ">=3.0.0 <=4.0.0"
dependencies: dependencies:
dio: ^5.3.2 win32: ^5.5.4
win32: 3.0.0 ffi: ^2.1.3
ffi: ^2.1.0 path: ^1.9.0
path: ^1.8.3 http: ^1.2.2
http: ^1.1.0 crypto: ^3.0.5
crypto: ^3.0.2 archive: ^3.6.1
archive: ^3.3.7
ini: ^2.1.0 ini: ^2.1.0
shelf_proxy: ^1.0.2 shelf_proxy: ^1.0.2
sync: ^0.3.0 sync: ^0.3.0
uuid: ^3.0.6 uuid: ^4.5.1
shelf_web_socket: ^2.0.0 shelf_web_socket: ^2.0.0
version: ^3.0.2 version: ^3.0.2
synchronized: ^3.3.0+3
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^5.0.0

View File

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

View File

@@ -1,16 +1,39 @@
# reboot_launcher
Launcher for project reboot # Reboot Launcher
Welcome to the **Reboot Launcher**!
This is a GUI application developed as part of the **Reboot Project**.
## Getting Started ## Getting Started
This project is a starting point for a Flutter application. ### Running the Project
To launch the project in development mode, simply run:
```
flutter run
```
A few resources to get you started if this is your first Flutter project: ### Building the Project
To create a production-ready build, use:
```
flutter build
```
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) ### Packaging the Project
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) To package the application for distribution, run:
```
package.bat
```
For help getting started with Flutter development, view the ## Requirements
[online documentation](https://docs.flutter.dev/), which offers tutorials, - [Flutter SDK](https://flutter.dev/docs/get-started/install)
samples, guidance on mobile development, and a full API reference. - Supported operating systems: Windows
## Other platforms
Native support for these platforms is not currently planned, but Linux support is a priority for the 10.0 release cycle
- [Linux Tutorial using Proton](https://www.reddit.com/r/linux_gaming/comments/1fwa4l8/guide_running_a_fortnite_private_server_to_play/)
- No tutorials are available for MacOS(got lost when the Reboot discord was banned), but it's possible to run Reboot using a compatibility layer
## Contributing
Contributions are welcome! Feel free to open an issue or submit a pull request.

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 0 B

View File

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

View File

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

File diff suppressed because it is too large Load Diff

BIN
gui/assets/build/aria2c.exe Normal file

Binary file not shown.

View File

@@ -1,2 +0,0 @@
taskkill /f /im winrar.exe
taskkill /f /im tar.exe

View File

@@ -1,735 +0,0 @@
[Code]
// https://github.com/DomGries/InnoDependencyInstaller
// types and variables
type
TDependency_Entry = record
Filename: String;
Parameters: String;
Title: String;
URL: String;
Checksum: String;
ForceSuccess: Boolean;
RestartAfter: Boolean;
end;
var
Dependency_Memo: String;
Dependency_List: array of TDependency_Entry;
Dependency_NeedToRestart, Dependency_ForceX86: Boolean;
Dependency_DownloadPage: TDownloadWizardPage;
procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean);
var
Dependency: TDependency_Entry;
DependencyCount: Integer;
begin
Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title;
Dependency.Filename := Filename;
Dependency.Parameters := Parameters;
Dependency.Title := Title;
if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin
Dependency.URL := '';
end else begin
Dependency.URL := URL;
end;
Dependency.Checksum := Checksum;
Dependency.ForceSuccess := ForceSuccess;
Dependency.RestartAfter := RestartAfter;
DependencyCount := GetArrayLength(Dependency_List);
SetArrayLength(Dependency_List, DependencyCount + 1);
Dependency_List[DependencyCount] := Dependency;
end;
<event('InitializeWizard')>
procedure Dependency_InitializeWizard;
begin
Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
end;
<event('PrepareToInstall')>
function Dependency_PrepareToInstall(var NeedsRestart: Boolean): String;
var
DependencyCount, DependencyIndex, ResultCode: Integer;
Retry: Boolean;
TempValue: String;
begin
DependencyCount := GetArrayLength(Dependency_List);
if DependencyCount > 0 then begin
Dependency_DownloadPage.Show;
for DependencyIndex := 0 to DependencyCount - 1 do begin
if Dependency_List[DependencyIndex].URL <> '' then begin
Dependency_DownloadPage.Clear;
Dependency_DownloadPage.Add(Dependency_List[DependencyIndex].URL, Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Checksum);
Retry := True;
while Retry do begin
Retry := False;
try
Dependency_DownloadPage.Download;
except
if Dependency_DownloadPage.AbortedByUser then begin
Result := Dependency_List[DependencyIndex].Title;
DependencyIndex := DependencyCount;
end else begin
case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
IDABORT: begin
Result := Dependency_List[DependencyIndex].Title;
DependencyIndex := DependencyCount;
end;
IDRETRY: begin
Retry := True;
end;
end;
end;
end;
end;
end;
end;
if Result = '' then begin
for DependencyIndex := 0 to DependencyCount - 1 do begin
Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, '');
Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1);
while True do begin
ResultCode := 0;
#ifdef Dependency_CustomExecute
if {#Dependency_CustomExecute}(ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, ResultCode) then begin
#else
if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin
#endif
if Dependency_List[DependencyIndex].RestartAfter then begin
if DependencyIndex = DependencyCount - 1 then begin
Dependency_NeedToRestart := True;
end else begin
NeedsRestart := True;
Result := Dependency_List[DependencyIndex].Title;
end;
break;
end else if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0)
break;
end else if ResultCode = 1641 then begin // ERROR_SUCCESS_REBOOT_INITIATED (1641)
NeedsRestart := True;
Result := Dependency_List[DependencyIndex].Title;
break;
end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010)
Dependency_NeedToRestart := True;
break;
end;
end;
case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependency_List[DependencyIndex].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
IDABORT: begin
Result := Dependency_List[DependencyIndex].Title;
break;
end;
IDIGNORE: begin
break;
end;
end;
end;
if Result <> '' then begin
break;
end;
end;
if NeedsRestart then begin
TempValue := '"' + ExpandConstant('{srcexe}') + '" /restart=1 /LANG="' + ExpandConstant('{language}') + '" /DIR="' + WizardDirValue + '" /GROUP="' + WizardGroupValue + '" /TYPE="' + WizardSetupType(False) + '" /COMPONENTS="' + WizardSelectedComponents(False) + '" /TASKS="' + WizardSelectedTasks(False) + '"';
if WizardNoIcons then begin
TempValue := TempValue + ' /NOICONS';
end;
RegWriteStringValue(HKA, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', '{#SetupSetting("AppName")}', TempValue);
end;
end;
Dependency_DownloadPage.Hide;
end;
end;
#ifndef Dependency_NoUpdateReadyMemo
<event('UpdateReadyMemo')>
#endif
function Dependency_UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
begin
Result := '';
if MemoUserInfoInfo <> '' then begin
Result := Result + MemoUserInfoInfo + Newline + NewLine;
end;
if MemoDirInfo <> '' then begin
Result := Result + MemoDirInfo + Newline + NewLine;
end;
if MemoTypeInfo <> '' then begin
Result := Result + MemoTypeInfo + Newline + NewLine;
end;
if MemoComponentsInfo <> '' then begin
Result := Result + MemoComponentsInfo + Newline + NewLine;
end;
if MemoGroupInfo <> '' then begin
Result := Result + MemoGroupInfo + Newline + NewLine;
end;
if MemoTasksInfo <> '' then begin
Result := Result + MemoTasksInfo;
end;
if Dependency_Memo <> '' then begin
if MemoTasksInfo = '' then begin
Result := Result + SetupMessage(msgReadyMemoTasks);
end;
Result := Result + FmtMessage(Dependency_Memo, [Space]);
end;
end;
<event('NeedRestart')>
function Dependency_NeedRestart: Boolean;
begin
Result := Dependency_NeedToRestart;
end;
function Dependency_IsX64: Boolean;
begin
Result := not Dependency_ForceX86 and Is64BitInstallMode;
end;
function Dependency_String(const x86, x64: String): String;
begin
if Dependency_IsX64 then begin
Result := x64;
end else begin
Result := x86;
end;
end;
function Dependency_ArchSuffix: String;
begin
Result := Dependency_String('', '_x64');
end;
function Dependency_ArchTitle: String;
begin
Result := Dependency_String(' (x86)', ' (x64)');
end;
function Dependency_IsNetCoreInstalled(const Version: String): Boolean;
var
ResultCode: Integer;
begin
// source code: https://github.com/dotnet/deployment-tools/tree/main/src/clickonce/native/projects/NetCoreCheck
if not FileExists(ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe') then begin
ExtractTemporaryFile('netcorecheck' + Dependency_ArchSuffix + '.exe');
end;
Result := ShellExec('', ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe', Version, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
end;
procedure Dependency_AddDotNet35;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net35-sp1
if not IsDotNetInstalled(net35, 1) then begin
Dependency_Add('dotnetfx35.exe',
'/lang:enu /passive /norestart',
'.NET Framework 3.5 Service Pack 1',
'https://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe',
'', False, False);
end;
end;
procedure Dependency_AddDotNet40;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net40
if not IsDotNetInstalled(net4full, 0) then begin
Dependency_Add('dotNetFx40_Full_setup.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.0',
'https://download.microsoft.com/download/1/B/E/1BE39E79-7E39-46A3-96FF-047F95396215/dotNetFx40_Full_setup.exe',
'', False, False);
end;
end;
procedure Dependency_AddDotNet45;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net452
if not IsDotNetInstalled(net452, 0) then begin
Dependency_Add('dotnetfx45.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.5.2',
'https://go.microsoft.com/fwlink/?LinkId=397707',
'', False, False);
end;
end;
procedure Dependency_AddDotNet46;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net462
if not IsDotNetInstalled(net462, 0) then begin
Dependency_Add('dotnetfx46.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.6.2',
'https://go.microsoft.com/fwlink/?linkid=780596',
'', False, False);
end;
end;
procedure Dependency_AddDotNet47;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net472
if not IsDotNetInstalled(net472, 0) then begin
Dependency_Add('dotnetfx47.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.7.2',
'https://go.microsoft.com/fwlink/?LinkId=863262',
'', False, False);
end;
end;
procedure Dependency_AddDotNet48;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net48
if not IsDotNetInstalled(net48, 0) then begin
Dependency_Add('dotnetfx48.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.8',
'https://go.microsoft.com/fwlink/?LinkId=2085155',
'', False, False);
end;
end;
procedure Dependency_AddDotNet481;
var
Version: Cardinal;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net481
if not RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', Version) or (Version < 533320) then begin
Dependency_Add('dotnetfx481.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.8.1',
'https://go.microsoft.com/fwlink/?LinkId=2203304',
'', False, False);
end;
end;
procedure Dependency_AddNetCore31;
begin
// https://dotnet.microsoft.com/download/dotnet-core/3.1
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 3.1.32') then begin
Dependency_Add('netcore31' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Core Runtime 3.1.32' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/de4b3438-24a2-4d1d-a845-97355cf97b71/515abb880478b49f7c1bced8fbf07b16/dotnet-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/476eba79-f17f-49c8-a213-0f24a22cd026/37c02de81ff5b76ac57a5427462395f1/dotnet-runtime-3.1.32-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddNetCore31Asp;
begin
// https://dotnet.microsoft.com/download/dotnet-core/3.1
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 3.1.32') then begin
Dependency_Add('netcore31asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 3.1.32' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/63b482d2-04b2-4dd4-baaf-d1e78de80738/40321091c872f4e77337b68fc61a5a07/aspnetcore-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/98910750-2644-472c-ab2b-17f315ccb953/c2a4c223ee11e2eec7d13744e7a45547/aspnetcore-runtime-3.1.32-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddNetCore31Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet-core/3.1
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 3.1.32') then begin
Dependency_Add('netcore31desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 3.1.32' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3f353d2c-0431-48c5-bdf6-fbbe8f901bb5/542a4af07c1df5136a98a1c2df6f3d62/windowsdesktop-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b92958c6-ae36-4efa-aafe-569fced953a5/1654639ef3b20eb576174c1cc200f33a/windowsdesktop-runtime-3.1.32-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet50;
begin
// https://dotnet.microsoft.com/download/dotnet/5.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 5.0.17') then begin
Dependency_Add('dotnet50' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 5.0.17' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/54683c13-6b04-4d7d-b4d4-1f055b50ea43/e99048e2840d57040e8312058853a5b9/dotnet-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a0832b5a-6900-442b-af79-6ffddddd6ba4/e2df0b25dd851ee0b38a86947dd0e42e/dotnet-runtime-5.0.17-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet50Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/5.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 5.0.17') then begin
Dependency_Add('dotnet50asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 5.0.17' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/4bfa247d-321d-4b29-a34b-62320849059b/8df7a17d9aad4044efe9b5b1c423e82c/aspnetcore-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3789ec90-2717-424f-8b9c-3adbbcea6c16/2085cc5ff077b8789ff938015392e406/aspnetcore-runtime-5.0.17-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet50Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/5.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 5.0.17') then begin
Dependency_Add('dotnet50desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 5.0.17' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b6fe5f2a-95f4-46f1-9824-f5994f10bc69/db5ec9b47ec877b5276f83a185fdb6a0/windowsdesktop-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3aa4e942-42cd-4bf5-afe7-fc23bd9c69c5/64da54c8864e473c19a7d3de15790418/windowsdesktop-runtime-5.0.17-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet60;
begin
// https://dotnet.microsoft.com/download/dotnet/6.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 6.0.20') then begin
Dependency_Add('dotnet60' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 6.0.20' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3be5ee3a-c171-4cd2-ab98-00ca5c11eb8c/6fd31294b0c6c670ab5c060592935203/dotnet-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3cfb6d2a-afbe-4ae7-8e5b-776f350654cc/6e8d858a60fe15381f3c84d8ca66c4a7/dotnet-runtime-6.0.20-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet60Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/6.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 6.0.20') then begin
Dependency_Add('dotnet60asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 6.0.20' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0e37c76c-53b4-4eea-8f5c-6ad2f8d5fe3c/88a8620329ced1aee271992a5b56d236/aspnetcore-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/be9f67fd-60af-45b1-9bca-a7bcc0e86e7e/6a750f7d7432937b3999bb4c5325062a/aspnetcore-runtime-6.0.20-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet60Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/6.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 6.0.20') then begin
Dependency_Add('dotnet60desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 6.0.20' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0413b619-3eb2-4178-a78e-8d1aafab1a01/5247f08ea3c13849b68074a2142fbf31/windowsdesktop-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1146f414-17c7-4184-8b10-1addfa5315e4/39db5573efb029130add485566320d74/windowsdesktop-runtime-6.0.20-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet70;
begin
// https://dotnet.microsoft.com/download/dotnet/7.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 7.0.9') then begin
Dependency_Add('dotnet70' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 7.0.9' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/305a85f5-2b0d-459b-b2ea-caf71b98d25d/805edc610efa49432e5e268bbba4eacb/dotnet-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/73058888-02a4-4f6d-b3cd-845531c2d7d0/a785e54b7f12046c00714b2ba759e173/dotnet-runtime-7.0.9-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet70Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/7.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 7.0.9') then begin
Dependency_Add('dotnet70asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 7.0.9' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/6ec3b357-31df-4b18-948f-4979a5b4b99f/fdeec71fc7f0f34ecfa0cb8b2b897da0/aspnetcore-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/edd9c9b1-0c49-4297-9197-9392b2462318/d06fedaefb256d801ce94ade76af3ad9/aspnetcore-runtime-7.0.9-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet70Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/7.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 7.0.9') then begin
Dependency_Add('dotnet70desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 7.0.9' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/139b19d0-2d39-48ce-b59a-aec437509c20/ea6a2711eec53660c3b14d78b9fb2963/windowsdesktop-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/7727acb3-25ca-473b-a392-75afeb33cab7/f11f0477fd2fcfbb3111881377d0c9bb/windowsdesktop-runtime-7.0.9-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet80;
begin
// https://dotnet.microsoft.com/download/dotnet/8.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 8.0.3') then begin
Dependency_Add('dotnet80' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 8.0.3' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c8d7a77c-5647-4e38-9ed8-edf82328497d/56130e071ac13c3660b0f3a0d60914c7/dotnet-runtime-8.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/961dfc84-ea72-48a2-b3f4-b82cefc34580/6ac50b6bf244a2c5481ad705a92cf843/dotnet-runtime-8.0.3-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet80Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/8.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 8.0.3') then begin
Dependency_Add('dotnet80asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 8.0.3' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/e1efd12b-9598-4b70-ad83-496563ae3f7c/da67696e4232886f52d50bb8ecda5ab1/aspnetcore-runtime-8.0.3-win-x86.zip', 'https://download.visualstudio.microsoft.com/download/pr/e91876a9-1760-42cb-a6f4-97c57e9cca52/b433fcf4768929539f17e1908cb315bf/aspnetcore-runtime-8.0.3-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet80Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/8.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 8.0.3') then begin
Dependency_Add('dotnet80desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 8.0.3' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c629f243-5125-4751-a5ff-e78fa45646b1/85777e3e3f58f863d884fd4b8a1453f2/windowsdesktop-runtime-8.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/51bc18ac-0594-412d-bd63-18ece4c91ac4/90b47b97c3bfe40a833791b166697e67/windowsdesktop-runtime-8.0.3-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2005;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=26347
if not IsMsiProductInstalled(Dependency_String('{86C9D5AA-F00C-4921-B3F2-C60AF92E2844}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}'), PackVersionComponents(8, 0, 61000, 0)) then begin
Dependency_Add('vcredist2005' + Dependency_ArchSuffix + '.exe',
'/q',
'Visual C++ 2005 Service Pack 1 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'),
'', False, False);
end;
end;
procedure Dependency_AddVC2008;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=26368
if not IsMsiProductInstalled(Dependency_String('{DE2C306F-A067-38EF-B86C-03DE4B0312F9}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}'), PackVersionComponents(9, 0, 30729, 6161)) then begin
Dependency_Add('vcredist2008' + Dependency_ArchSuffix + '.exe',
'/q',
'Visual C++ 2008 Service Pack 1 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2010;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=26999
if not IsMsiProductInstalled(Dependency_String('{1F4F1D2A-D9DA-32CF-9909-48485DA06DD5}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}'), PackVersionComponents(10, 0, 40219, 0)) then begin
Dependency_Add('vcredist2010' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2010 Service Pack 1 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2012;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=30679
if not IsMsiProductInstalled(Dependency_String('{4121ED58-4BD9-3E7B-A8B5-9F8BAAE045B7}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}'), PackVersionComponents(11, 0, 61030, 0)) then begin
Dependency_Add('vcredist2012' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2012 Update 4 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2013;
begin
// https://support.microsoft.com/en-us/help/4032938
if not IsMsiProductInstalled(Dependency_String('{B59F5BF1-67C8-3802-8E59-2CE551A39FC5}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}'), PackVersionComponents(12, 0, 40664, 0)) then begin
Dependency_Add('vcredist2013' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2013 Update 5 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2015To2022;
begin
// https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
if not IsMsiProductInstalled(Dependency_String('{65E5BD06-6392-3027-8C26-853107D3CF1A}', '{36F68A90-239C-34DF-B58C-64B30153CE35}'), PackVersionComponents(14, 30, 30704, 0)) then begin
Dependency_Add('vcredist2022' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2015-2022 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://aka.ms/vs/17/release/vc_redist.x86.exe', 'https://aka.ms/vs/17/release/vc_redist.x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDirectX;
begin
#ifdef Dependency_Files_DirectX
ExtractTemporaryFile('dxwebsetup.exe');
#endif
// https://www.microsoft.com/en-us/download/details.aspx?id=35
Dependency_Add('dxwebsetup.exe',
'/q',
'DirectX Runtime',
'https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe',
'', True, False);
end;
procedure Dependency_AddSql2008Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=30438
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(10, 50, 4000, 0)) < 0) then begin
Dependency_Add('sql2008express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2008 R2 Service Pack 2 Express',
Dependency_String('https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe'),
'', False, False);
end;
end;
procedure Dependency_AddSql2012Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=56042
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(11, 0, 7001, 0)) < 0) then begin
Dependency_Add('sql2012express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2012 Service Pack 4 Express',
Dependency_String('https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe'),
'', False, False);
end;
end;
procedure Dependency_AddSql2014Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=57473
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(12, 0, 6024, 0)) < 0) then begin
Dependency_Add('sql2014express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2014 Service Pack 3 Express',
Dependency_String('https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe'),
'', False, False);
end;
end;
procedure Dependency_AddSql2016Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=103447
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(13, 0, 6404, 1)) < 0) then begin
Dependency_Add('sql2016express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2016 Service Pack 3 Express',
'https://download.microsoft.com/download/f/a/8/fa83d147-63d1-449c-b22d-5fef9bd5bb46/SQLServer2016-SSEI-Expr.exe',
'', False, False);
end;
end;
procedure Dependency_AddSql2017Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=55994
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(14, 0, 0, 0)) < 0) then begin
Dependency_Add('sql2017express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2017 Express',
'https://download.microsoft.com/download/5/E/9/5E9B18CC-8FD5-467E-B5BF-BADE39C51F73/SQLServer2017-SSEI-Expr.exe',
'', False, False);
end;
end;
procedure Dependency_AddSql2019Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=101064
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(15, 0, 0, 0)) < 0) then begin
Dependency_Add('sql2019express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2019 Express',
'https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe',
'', False, False);
end;
end;
procedure Dependency_AddSql2022Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=104781
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(16, 0, 1000, 6)) < 0) then begin
Dependency_Add('sql2022express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2022 Express',
'https://go.microsoft.com/fwlink/p/?linkid=2216019',
'', False, False);
end;
end;
procedure Dependency_AddWebView2;
begin
// https://developer.microsoft.com/en-us/microsoft-edge/webview2
if not RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') then begin
Dependency_Add('MicrosoftEdgeWebview2Setup.exe',
'/silent /install',
'WebView2 Runtime',
'https://go.microsoft.com/fwlink/p/?LinkId=2124703',
'', False, False);
end;
end;
procedure Dependency_AddAccessDatabaseEngine2010;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=13255
if not RegKeyExists(HKLM, 'SOFTWARE\Microsoft\Office\14.0\Access Connectivity Engine\Engines\ACE') then begin
Dependency_Add('AccessDatabaseEngine2010' + Dependency_ArchSuffix + '.exe',
'/quiet',
'Microsoft Access Database Engine 2010' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine.exe', 'https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine_X64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddAccessDatabaseEngine2016;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=54920
if not RegKeyExists(HKLM, 'SOFTWARE\Microsoft\Office\16.0\Access Connectivity Engine\Engines\ACE') then begin
Dependency_Add('AccessDatabaseEngine2016' + Dependency_ArchSuffix + '.exe',
'/quiet',
'Microsoft Access Database Engine 2016' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine.exe', 'https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine_X64.exe'),
'', False, False);
end;
end;
[Files]
#ifdef Dependency_Path_NetCoreCheck
; download netcorecheck.exe: https://www.nuget.org/packages/Microsoft.NET.Tools.NETCoreCheck.x86
; download netcorecheck_x64.exe: https://www.nuget.org/packages/Microsoft.NET.Tools.NETCoreCheck.x64
Source: "{#Dependency_Path_NetCoreCheck}netcorecheck.exe"; Flags: dontcopy noencryption
Source: "{#Dependency_Path_NetCoreCheck}netcorecheck_x64.exe"; Flags: dontcopy noencryption
#endif
#ifdef Dependency_Path_DirectX
Source: "{#Dependency_Path_DirectX}dxwebsetup.exe"; Flags: dontcopy noencryption
#endif

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -76,10 +76,10 @@
"playGameServerCustomContent": "Enter IP", "playGameServerCustomContent": "Enter IP",
"settingsName": "Settings", "settingsName": "Settings",
"settingsClientName": "Internal files", "settingsClientName": "Internal files",
"settingsClientDescription": "Configure the internal files used by the launcher for Fortnite", "settingsClientDescription": "Configure the internal files used by the launcher",
"settingsClientOptionsName": "Options", "settingsClientOptionsName": "Options",
"settingsClientOptionsDescription": "Configure additional options for Fortnite", "settingsClientOptionsDescription": "Configure additional options for Fortnite",
"settingsClientConsoleName": "Unreal engine console", "settingsClientConsoleName": "Unreal engine patcher",
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console", "settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
"settingsClientConsoleKeyName": "Unreal engine console key", "settingsClientConsoleKeyName": "Unreal engine console key",
"settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console", "settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console",
@@ -88,24 +88,25 @@
"settingsClientMemoryName": "Memory patcher", "settingsClientMemoryName": "Memory patcher",
"settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak", "settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak",
"settingsClientArgsName": "Custom launch arguments", "settingsClientArgsName": "Custom launch arguments",
"settingsClientArgsDescription": "Additional arguments to use when launching the game", "settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
"settingsClientArgsPlaceholder": "Arguments...", "settingsClientArgsPlaceholder": "Arguments...",
"settingsServerName": "Internal files", "settingsServerName": "Internal files",
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server", "settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
"settingsServerOptionsName": "Options", "settingsServerOptionsName": "Options",
"settingsServerOptionsSubtitle": "Configure additional options for the game server", "settingsServerOptionsSubtitle": "Configure additional options for the game server",
"settingsServerTypeName": "Type", "settingsServerTypeName": "Game server type",
"settingsServerTypeDescription": "The type of game server to inject", "settingsServerTypeDescription": "The type of game server to inject",
"settingsServerTypeEmbeddedName": "Embedded", "settingsServerTypeEmbeddedName": "Embedded",
"settingsServerTypeCustomName": "Custom", "settingsServerTypeCustomName": "Custom",
"settingsServerFileName": "Implementation", "settingsOldServerFileName": "Game server",
"settingsServerFileDescription": "The file injected to create the game server", "settingsServerFileDescription": "The file injected to create the game server",
"settingsServerPortName": "Port", "settingsServerPortName": "Port",
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on", "settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
"settingsServerMirrorName": "Update mirror", "settingsServerOldMirrorName": "Update mirror (Before season 20)",
"settingsServerNewMirrorName": "Update mirror (Season 20 and above)",
"settingsServerMirrorDescription": "The URL used to update the game server dll", "settingsServerMirrorDescription": "The URL used to update the game server dll",
"settingsServerMirrorPlaceholder": "mirror", "settingsServerMirrorPlaceholder": "mirror",
"settingsServerTimerName": "Update timer", "settingsServerTimerName": "Game server updater",
"settingsServerTimerSubtitle": "Determines when the game server should be updated", "settingsServerTimerSubtitle": "Determines when the game server should be updated",
"settingsUtilsName": "Launcher", "settingsUtilsName": "Launcher",
"settingsUtilsSubtitle": "This section contains settings related to the launcher", "settingsUtilsSubtitle": "This section contains settings related to the launcher",
@@ -118,9 +119,7 @@
"settingsUtilsResetDefaultsName": "Reset settings", "settingsUtilsResetDefaultsName": "Reset settings",
"settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values", "settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values",
"settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible", "settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
"settingsUtilsResetDefaultsContent": "Reset",
"settingsUtilsDialogSecondaryAction": "Close", "settingsUtilsDialogSecondaryAction": "Close",
"settingsUtilsDialogPrimaryAction": "Reset",
"selectFortniteName": "Fortnite version", "selectFortniteName": "Fortnite version",
"selectFortniteDescription": "Select the version of Fortnite you want to use", "selectFortniteDescription": "Select the version of Fortnite you want to use",
"manageVersionsName": "Manage versions", "manageVersionsName": "Manage versions",
@@ -129,10 +128,11 @@
"importVersionDescription": "Import a new version of Fortnite into the launcher", "importVersionDescription": "Import a new version of Fortnite into the launcher",
"addLocalBuildName": "Add a version from this PC's local storage", "addLocalBuildName": "Add a version from this PC's local storage",
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work", "addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
"addVersion": "Add version", "addVersion": "Import",
"downloadBuildName": "Download any version from the cloud", "downloadBuildName": "Download any version from the cloud",
"downloadBuildDescription": "Download any Fortnite build easily from the cloud", "downloadBuildDescription": "Download any Fortnite build easily from the cloud",
"downloadBuildContent": "Download build", "downloadBuildContent": "Download build",
"downloadVersion": "Download",
"cannotUpdateGameServer": "An error occurred while updating the game server: {error}", "cannotUpdateGameServer": "An error occurred while updating the game server: {error}",
"launchFortnite": "Launch Fortnite", "launchFortnite": "Launch Fortnite",
"closeFortnite": "Close Fortnite", "closeFortnite": "Close Fortnite",
@@ -149,6 +149,7 @@
"downloadingDll": "Downloading {name} dll...", "downloadingDll": "Downloading {name} dll...",
"downloadDllSuccess": "The {name} dll was downloaded successfully", "downloadDllSuccess": "The {name} dll was downloaded successfully",
"downloadDllError": "An error occurred while downloading {name}: {error}", "downloadDllError": "An error occurred while downloading {name}: {error}",
"downloadDllAntivirus": "The {name} dll was deleted: your antivirus({antivirus}) might have flagged it",
"downloadDllRetry": "Retry", "downloadDllRetry": "Retry",
"uncaughtErrorMessage": "An uncaught error was thrown: {error}", "uncaughtErrorMessage": "An uncaught error was thrown: {error}",
"launchingGameServer": "Launching the game server...", "launchingGameServer": "Launching the game server...",
@@ -156,6 +157,7 @@
"launchingGameClientAndServer": "Launching the game client and server...", "launchingGameClientAndServer": "Launching the game client and server...",
"startGameServer": "Start a game server", "startGameServer": "Start a game server",
"usernameOrEmail": "Username/Email", "usernameOrEmail": "Username/Email",
"invalidEmail": "Invalid email",
"usernameOrEmailPlaceholder": "Type your username or email", "usernameOrEmailPlaceholder": "Type your username or email",
"password": "Password", "password": "Password",
"passwordPlaceholder": "Type your password, if you want to use one", "passwordPlaceholder": "Type your password, if you want to use one",
@@ -211,10 +213,12 @@
"selectBuild": "Select a fortnite version", "selectBuild": "Select a fortnite version",
"fetchingBuilds": "Fetching builds and disks...", "fetchingBuilds": "Fetching builds and disks...",
"unknownError": "Unknown error", "unknownError": "Unknown error",
"unknown": "unknown",
"downloadVersionError": "Cannot download version: {error}", "downloadVersionError": "Cannot download version: {error}",
"downloadedVersion": "The download was completed successfully!", "downloadedVersion": "The download was completed successfully!",
"download": "Download", "download": "Download",
"downloading": "Downloading...", "downloading": "Downloading...",
"startingDownload": "Starting download...",
"extracting": "Extracting...", "extracting": "Extracting...",
"buildProgress": "{progress}%", "buildProgress": "{progress}%",
"buildInstallationDirectory": "Installation directory", "buildInstallationDirectory": "Installation directory",
@@ -234,13 +238,13 @@
"startGame": "Start fortnite", "startGame": "Start fortnite",
"stopGame": "Close fortnite", "stopGame": "Close fortnite",
"waitingForGameServer": "Waiting for the game server to boot up...", "waitingForGameServer": "Waiting for the game server to boot up...",
"gameServerStartWarning": "The game server was started successfully, but Reboot didn't load", "gameServerStartWarning": "Unsupported version: the game server crashed while setting up the server",
"gameServerStartLocalWarning": "The game server was started successfully, but other players can't join", "gameServerStartLocalWarning": "The game server was started successfully, but other players can't join",
"gameServerStarted": "The game server was started successfully", "gameServerStarted": "The game server was started successfully",
"gameClientStarted": "The game client was started successfully", "gameClientStarted": "The game client was started successfully",
"checkingGameServer": "Checking if other players can join the game server...", "checkingGameServer": "Checking if other players can join the game server...",
"checkGameServerFixMessage": "Other players can't join the game server as port {port} isn't open", "checkGameServerFixMessage": "The game server was started successfully, but other players can't join yet as port {port} isn't open",
"checkGameServerFixAction": "Fix", "checkGameServerFixAction": "Learn more",
"infoName": "Info", "infoName": "Info",
"emptyVersionName": "Empty version name", "emptyVersionName": "Empty version name",
"versionAlreadyExists": "This version already exists", "versionAlreadyExists": "This version already exists",
@@ -256,11 +260,13 @@
"emptyURL": "Empty update URL", "emptyURL": "Empty update URL",
"missingVersionError": "Download or select a version before starting Fortnite", "missingVersionError": "Download or select a version before starting Fortnite",
"missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted", "missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted",
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version", "multipleExecutablesError": "There must be only one executable named {name} in the game directory",
"corruptedVersionError": "Fortnite crashed while starting: either the game installation is corrupted or an injected dll({dlls}) tried to access memory illegally",
"corruptedDllError": "Cannot inject dll: {error}", "corruptedDllError": "Cannot inject dll: {error}",
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings", "missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})", "tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}", "unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
"fortniteCrashError": "The {name} crashed after being launched",
"serverNoLongerAvailableUnnamed": "The previous server is no longer available", "serverNoLongerAvailableUnnamed": "The previous server is no longer available",
"noServerFound": "No server found: invalid or expired link", "noServerFound": "No server found: invalid or expired link",
"settingsUtilsThemeName": "Theme", "settingsUtilsThemeName": "Theme",
@@ -279,9 +285,9 @@
"infoVideoName": "Tutorial", "infoVideoName": "Tutorial",
"infoVideoDescription": "Show the tutorial again in the launcher", "infoVideoDescription": "Show the tutorial again in the launcher",
"infoVideoContent": "Start Tutorial", "infoVideoContent": "Start Tutorial",
"dllDeletedTitle": "A critical dll was deleted. If you didn't delete it, your Antivirus probably flagged it. This is a false positive: please disable your Antivirus and try again", "dllDeletedTitle": "A critical dll was deleted and couldn't be reinstalled",
"dllDeletedSecondaryAction": "Close", "dllDeletedSecondaryAction": "Close",
"dllDeletedPrimaryAction": "Try again", "dllDeletedPrimaryAction": "Disable Antivirus",
"clickKey": "Waiting for a key to be registered", "clickKey": "Waiting for a key to be registered",
"settingsLogsName": "Export logs", "settingsLogsName": "Export logs",
"settingsLogsDescription": "Exports an archive containing all the logs produced by the launcher", "settingsLogsDescription": "Exports an archive containing all the logs produced by the launcher",
@@ -303,11 +309,8 @@
"quizZeroTriesLeft": "zero tries", "quizZeroTriesLeft": "zero tries",
"quizOneTryLeft": "one try", "quizOneTryLeft": "one try",
"quizTwoTriesLeft": "two tries", "quizTwoTriesLeft": "two tries",
"gameServerTypeName": "Type", "gameServerTypeName": "Headless",
"gameServerTypeDescription": "The type of game server to use", "gameServerTypeDescription": "Disables game rendering to save resources",
"gameServerTypeHeadless": "Background process",
"gameServerTypeVirtualWindow": "Virtual window",
"gameServerTypeWindow": "Normal window",
"localBuild": "This PC", "localBuild": "This PC",
"githubArchive": "Cloud archive", "githubArchive": "Cloud archive",
"all": "All", "all": "All",
@@ -320,8 +323,11 @@
"none": "none", "none": "none",
"openLog": "Open log", "openLog": "Open log",
"backendProcessError": "The backend shut down unexpectedly", "backendProcessError": "The backend shut down unexpectedly",
"backendErrorMessage": "The backend reported an unexpected error",
"welcomeTitle": "Welcome to Reboot Launcher", "welcomeTitle": "Welcome to Reboot Launcher",
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab", "welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
"hostAccountText": "The host tab shows different credentials compared to the play tab.\nIf you are advanced user, you can set a different email and password\nhere if the backend you are using needs authentication.",
"hostAccountAction": "I understand",
"welcomeAction": "Take the tour", "welcomeAction": "Take the tour",
"startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.", "startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.",
"startOnboardingActionLabel": "Let's do it", "startOnboardingActionLabel": "Let's do it",
@@ -359,9 +365,23 @@
"promptBackendDetachedActionLabel": "Next", "promptBackendDetachedActionLabel": "Next",
"promptInfoTabText": "The Info tab contains useful links to report bugs and receive support", "promptInfoTabText": "The Info tab contains useful links to report bugs and receive support",
"promptInfoTabActionLabel": "Next", "promptInfoTabActionLabel": "Next",
"promptSettingsTabText": "The Settings tab contains options to customize and reset the launcher", "promptSettingsTabText": "The Settings tab contains options to customize the launcher",
"promptSettingsTabActionLabel": "Done", "promptSettingsTabActionLabel": "Done",
"automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!", "automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!",
"automaticGameServerDialogIgnore": "Ignore", "automaticGameServerDialogIgnore": "Ignore",
"automaticGameServerDialogStart": "Start server" "automaticGameServerDialogStart": "Start server",
"gameResetDefaultsName": "Reset",
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
"gameResetDefaultsContent": "Reset",
"selectFile": "Select a file",
"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

@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
@@ -12,16 +11,16 @@ import 'package:local_notifier/local_notifier.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/error.dart'; import 'package:reboot_launcher/src/widget/message/error.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/widget/page/home_page.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/url_protocol.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
import 'package:url_protocol/url_protocol.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@@ -83,9 +82,7 @@ Future<void> _startApp() async {
errors.add(uncaughtError); errors.add(uncaughtError);
} finally{ } finally{
log("[APP] Started applications with errors: $errors"); log("[APP] Started applications with errors: $errors");
runApp(RebootApplication( runApp(RebootApplication(errors: errors));
errors: errors,
));
} }
} }
@@ -146,54 +143,56 @@ Future<Object?> _initVersion() async {
Future<Object?> _initUrlHandler() async { Future<Object?> _initUrlHandler() async {
try { try {
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']); registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']);
return null; return null;
}catch(error) { }catch(error) {
return error; return error;
} }
} }
void _initWindow() => doWhenWindowReady(() async { Future<void> _initWindow() async {
try { try {
await SystemTheme.accentColor.load(); await SystemTheme.accentColor.load();
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await Window.initialize(); await Window.initialize();
var settingsController = Get.find<SettingsController>(); var settingsController = Get.find<SettingsController>();
var size = Size(settingsController.width, settingsController.height); var size = Size(settingsController.width, settingsController.height);
appWindow.size = size; await windowManager.setSize(size);
var offsetX = settingsController.offsetX; var offsetX = settingsController.offsetX;
var offsetY = settingsController.offsetY; var offsetY = settingsController.offsetY;
if(offsetX != null && offsetY != null) { if(offsetX != null && offsetY != null) {
appWindow.position = Offset( final position = Offset(
offsetX, offsetX,
offsetY offsetY
); );
await windowManager.setPosition(position);
}else { }else {
appWindow.alignment = Alignment.center; await windowManager.setAlignment(Alignment.center);
} }
await windowManager.setPreventClose(true);
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight); await windowManager.setResizable(true);
if(isWin11) { if(isWin11) {
await Window.setEffect( await Window.setEffect(
effect: WindowEffect.acrylic, effect: WindowEffect.acrylic,
color: Colors.transparent, color: Colors.green,
dark: isDarkMode dark: isDarkMode
); );
} }
}catch(error, stackTrace) { }catch(error, stackTrace) {
onError(error, stackTrace, false); onError(error, stackTrace, false);
}finally { }finally {
appWindow.show(); windowManager.show();
}
} }
});
Future<List<Object>> _initStorage() async { Future<List<Object>> _initStorage() async {
final errors = <Object>[]; final errors = <Object>[];
try { try {
await GetStorage("game_storage", settingsDirectory.path).initStorage; await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
await GetStorage("backend_storage", settingsDirectory.path).initStorage; await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
await GetStorage("settings_storage", settingsDirectory.path).initStorage; await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
await GetStorage("hosting_storage", settingsDirectory.path).initStorage; await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
}catch(error) { }catch(error) {
appWithNoStorage = true; appWithNoStorage = true;
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage"); errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
@@ -225,6 +224,11 @@ Future<List<Object>> _initStorage() async {
errors.add(error); errors.add(error);
} }
try {
Get.put(DllController());
}catch(error) {
errors.add(error);
}
return errors; return errors;
} }
@@ -247,7 +251,11 @@ class _RebootApplicationState extends State<RebootApplication> {
} }
void _handleErrors(List<Object?> errors) { void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false)); for(final error in errors) {
if(error != null) {
onError(error, null, false);
}
}
} }
@override @override

View File

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

View File

@@ -0,0 +1,378 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/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 = "v3_dll_storage";
late final GetStorage? _storage;
late final TextEditingController customGameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final Rx<UpdateTimer> timer;
late final TextEditingController beforeS20Mirror;
late final TextEditingController aboveS20Mirror;
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);
customGameServerDll = _createController("game_server", InjectableDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.auth);
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
beforeS20Mirror = TextEditingController(text: _storage?.read("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) {
final controller = TextEditingController(text: _storage?.read(key) ?? getDefaultDllPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
}
void resetGame() {
customGameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.auth);
}
void resetServer() {
gameServerPort.text = kDefaultGameServerPort;
timer.value = UpdateTimer.hour;
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
status.value = UpdateStatus.waiting;
customGameServer.value = false;
timestamp.value = null;
updateGameServerDll();
}
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
InfoBarEntry? infoBarEntry;
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
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();
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
_listenToFileEvents(InjectableDll.gameServer);
return true;
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
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();
final result = updateGameServerDll(
force: true,
silent: silent
);
completer.complete(result);
},
child: Text(translations.downloadDllRetry),
)
);
return completer.future;
}
}
(File, bool) getInjectableData(String version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.gameServer:
if(customGameServer.value) {
return (File(customGameServerDll.text), true);
}
return (_isS20(version) ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.auth:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memoryLeak:
final memoryFile = File(memoryLeakDll.text);
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
}
}
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:
return unrealEngineConsoleDll;
case InjectableDll.auth:
return backendDll;
case InjectableDll.gameServer:
return customGameServerDll;
case InjectableDll.memoryLeak:
return memoryLeakDll;
}
}
String getDefaultDllPath(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth:
return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak:
return "${dllsDirectory.path}\\memory.dll";
}
}
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
InfoBarEntry? entry;
try {
if (dll == InjectableDll.gameServer) {
return await updateGameServerDll(silent: silent);
}
if(!force && File(filePath).existsSync()) {
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...");
}
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] An error occurred while downloading $dll: $message");
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer<bool>();
await showRebootInfoBar(
translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(false),
action: Button(
onPressed: () async {
final result = await download(dll, filePath, silent: silent, force: force);
completer.complete(result);
},
child: Text(translations.downloadDllRetry),
)
);
return completer.future;
}
}
Future<void> downloadAndGuardDependencies() async {
for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
if(path.equals(controller.text, defaultPath)) {
await download(injectable, controller.text);
}
}
}
void _listenToFileEvents(InjectableDll injectable) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
void onFileEvent(FileSystemEvent event, String filePath) {
if (!path.equals(event.path, filePath)) {
return;
}
if(path.equals(filePath, defaultPath)) {
Get.find<GameController>()
.instance
.value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
_updateInput(injectable);
}
StreamSubscription subscribe(String filePath) => File(filePath)
.parent
.watch(events: FileSystemEvent.delete | FileSystemEvent.move)
.listen((event) => onFileEvent(event, filePath));
controller.addListener(() {
_subscriptions[injectable]?.cancel();
_subscriptions[injectable] = subscribe(controller.text);
});
_subscriptions[injectable] = subscribe(controller.text);
}
void _updateInput(InjectableDll injectable) {
switch(injectable) {
case InjectableDll.console:
settingsConsoleDllInputKey.currentState?.validate();
break;
case InjectableDll.auth:
settingsAuthDllInputKey.currentState?.validate();
break;
case InjectableDll.gameServer:
settingsGameServerDllInputKey.currentState?.validate();
break;
case InjectableDll.memoryLeak:
settingsMemoryDllInputKey.currentState?.validate();
break;
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
}

View File

@@ -1,31 +1,28 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:math';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/keyboard.dart'; import 'package:reboot_launcher/main.dart';
import 'package:version/version.dart';
import '../../main.dart';
class GameController extends GetxController { class GameController extends GetxController {
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); static const String storageName = "v3_game_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final TextEditingController username; late final TextEditingController username;
late final TextEditingController password; late final TextEditingController password;
late final TextEditingController customLaunchArgs; late final TextEditingController customLaunchArgs;
late final Rx<List<FortniteVersion>> versions; late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion; late final Rxn<FortniteVersion> selectedVersion;
late final RxBool started; late final RxBool started;
late final Rxn<GameInstance> instance; late final Rxn<GameInstance> instance;
late final Rx<PhysicalKeyboardKey> consoleKey;
GameController() { GameController() {
_storage = appWithNoStorage ? null : GetStorage("game_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]"); Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
final decodedVersions = decodedVersionsJson final decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry)) .map((entry) => FortniteVersion.fromJson(entry))
@@ -33,49 +30,17 @@ class GameController extends GetxController {
versions = Rx(decodedVersions); versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions()); versions.listen((data) => _saveVersions());
final decodedSelectedVersionName = _storage?.read("version"); final decodedSelectedVersionName = _storage?.read("version");
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName); selectedVersion = Rxn(decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName));
_selectedVersion = Rxn(decodedSelectedVersion); selectedVersion.listen((version) => _storage?.write("version", version?.name));
username = TextEditingController( username = TextEditingController(
text: _storage?.read("username") ?? kDefaultPlayerName); text: _storage?.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage?.write("username", username.text)); username.addListener(() => _storage?.write("username", username.text));
password = TextEditingController(text: _storage?.read("password") ?? ""); password = TextEditingController(text: _storage?.read("password") ?? "");
password.addListener(() => _storage?.write("password", password.text)); password.addListener(() => _storage?.write("password", password.text));
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? ""); customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_storage?.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false); started = RxBool(false);
instance = Rxn(); instance = Rxn();
consoleKey = Rx(_readConsoleKey());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
}
PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) {
return _kDefaultConsoleKey;
}
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
if(consoleKeyNumber == null) {
return _kDefaultConsoleKey;
}
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
if(!consoleKey.isUnrealEngineKey) {
return _kDefaultConsoleKey;
}
return consoleKey;
}
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
await defaultInput.parent.create(recursive: true);
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
} }
void reset() { void reset() {
@@ -83,25 +48,42 @@ class GameController extends GetxController {
password.text = ""; password.text = "";
customLaunchArgs.text = ""; customLaunchArgs.text = "";
versions.value = []; versions.value = [];
selectedVersion.value = null;
instance.value = null; instance.value = null;
} }
FortniteVersion? getVersionByName(String name) { FortniteVersion? getVersionByName(String name) {
return versions.value.firstWhereOrNull((element) => element.content.toString() == name); name = name.trim();
return versions.value.firstWhereOrNull((element) => element.name == name);
}
FortniteVersion? getVersionByGame(String gameVersion) {
gameVersion = gameVersion.trim();
final parsedGameVersion = Version.parse(gameVersion);
return versions.value.firstWhereOrNull((element) {
final compare = element.gameVersion.trim();
try {
final parsedCompare = Version.parse(compare);
return parsedCompare.major == parsedGameVersion.major
&& parsedCompare.minor == parsedGameVersion.minor;
} on FormatException {
return compare == gameVersion;
}
});
} }
void addVersion(FortniteVersion version) { void addVersion(FortniteVersion version) {
var empty = versions.value.isEmpty;
versions.update((val) => val?.add(version)); versions.update((val) => val?.add(version));
if(empty){ selectedVersion.value = version;
selectedVersion = version;
}
} }
void removeVersion(FortniteVersion version) { void removeVersion(FortniteVersion version) {
versions.update((val) => val?.remove(version)); final index = versions.value.indexOf(version);
if (selectedVersion == version || hasNoVersions) { versions.update((val) => val?.removeAt(index));
selectedVersion = null; if(hasNoVersions) {
selectedVersion.value = null;
}else {
selectedVersion.value = versions.value.elementAt(max(0, index - 1));
} }
} }
@@ -114,14 +96,5 @@ class GameController extends GetxController {
bool get hasNoVersions => versions.value.isEmpty; bool get hasNoVersions => versions.value.isEmpty;
FortniteVersion? get selectedVersion => _selectedVersion(); void updateVersion(FortniteVersion version, Function(FortniteVersion) function) => versions.update((val) => function(version));
set selectedVersion(FortniteVersion? version) {
_selectedVersion.value = version;
_storage?.write("version", version?.content.toString());
}
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
versions.update((val) => function(version));
}
} }

View File

@@ -12,8 +12,12 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class HostingController extends GetxController { class HostingController extends GetxController {
static const String storageName = "v3_hosting_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String uuid; late final String uuid;
late final TextEditingController accountUsername;
late final TextEditingController accountPassword;
late final TextEditingController name; late final TextEditingController name;
late final FocusNode nameFocusNode; late final FocusNode nameFocusNode;
late final TextEditingController description; late final TextEditingController description;
@@ -22,18 +26,23 @@ class HostingController extends GetxController {
late final FocusNode passwordFocusNode; late final FocusNode passwordFocusNode;
late final RxBool showPassword; late final RxBool showPassword;
late final RxBool discoverable; late final RxBool discoverable;
late final Rx<GameServerType> type; late final RxBool headless;
late final RxBool autoRestart; late final RxBool autoRestart;
late final RxBool started; late final RxBool started;
late final RxBool published; late final RxBool published;
late final Rxn<GameInstance> instance; late final Rxn<GameInstance> instance;
late final Rxn<Set<FortniteServer>> servers; late final Rxn<Set<FortniteServer>> servers;
late final TextEditingController customLaunchArgs;
late final Semaphore _semaphore; late final Semaphore _semaphore;
HostingController() { HostingController() {
_storage = appWithNoStorage ? null : GetStorage("hosting_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
uuid = _storage?.read("uuid") ?? const Uuid().v4(); uuid = _storage?.read("uuid") ?? const Uuid().v4();
_storage?.write("uuid", uuid); _storage?.write("uuid", uuid);
accountUsername = TextEditingController(text: _storage?.read("account_username") ?? kDefaultHostName);
accountUsername.addListener(() => _storage?.write("account_username", accountUsername.text));
accountPassword = TextEditingController(text: _storage?.read("account_password") ?? "");
accountPassword.addListener(() => _storage?.write("account_password", password.text));
name = TextEditingController(text: _storage?.read("name")); name = TextEditingController(text: _storage?.read("name"));
name.addListener(() => _storage?.write("name", name.text)); name.addListener(() => _storage?.write("name", name.text));
description = TextEditingController(text: _storage?.read("description")); description = TextEditingController(text: _storage?.read("description"));
@@ -45,24 +54,42 @@ class HostingController extends GetxController {
passwordFocusNode = FocusNode(); passwordFocusNode = FocusNode();
discoverable = RxBool(_storage?.read("discoverable") ?? false); discoverable = RxBool(_storage?.read("discoverable") ?? false);
discoverable.listen((value) => _storage?.write("discoverable", value)); discoverable.listen((value) => _storage?.write("discoverable", value));
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index)); headless = RxBool(_storage?.read("headless") ?? true);
type.listen((value) => _storage?.write("type", value.index)); headless.listen((value) => _storage?.write("headless", value));
autoRestart = RxBool(_storage?.read("auto_restart") ?? true); autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
autoRestart.listen((value) => _storage?.write("auto_restart", value)); autoRestart.listen((value) => _storage?.write("auto_restart", value));
started = RxBool(false); started = RxBool(false);
published = RxBool(false); published = RxBool(false);
showPassword = RxBool(false); showPassword = RxBool(false);
instance = Rxn(); instance = Rxn();
final supabase = Supabase.instance.client;
servers = Rxn(); servers = Rxn();
_listenServers();
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_semaphore = Semaphore();
}
void _listenServers([int attempt = 0]) {
log("[SUPABASE] Listening...");
final supabase = Supabase.instance.client;
supabase.from("hosting_v2") supabase.from("hosting_v2")
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet()) .map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
.listen((event) { .listen(
_onNewServer,
onError: (error) async {
log("[SUPABASE] Error: ${error}");
await Future.delayed(Duration(seconds: attempt * 5));
_listenServers(attempt + 1);
},
cancelOnError: true
);
}
void _onNewServer(Set<FortniteServer> event) {
log("[SUPABASE] New event: ${event}");
servers.value = event; servers.value = event;
published.value = event.any((element) => element.id == uuid); published.value = event.any((element) => element.id == uuid);
});
_semaphore = Semaphore();
} }
Future<void> publishServer(String author, String version) async { Future<void> publishServer(String author, String version) async {
@@ -131,14 +158,16 @@ class HostingController extends GetxController {
} }
void reset() { void reset() {
accountUsername.text = kDefaultHostName;
accountPassword.text = "";
name.text = ""; name.text = "";
description.text = ""; description.text = "";
showPassword.value = false; showPassword.value = false;
discoverable.value = false; discoverable.value = false;
started.value = false;
instance.value = null; instance.value = null;
type.value = GameServerType.headless; headless.value = true;
autoRestart.value = true; autoRestart.value = true;
customLaunchArgs.text = "";
} }
FortniteServer? findServerById(String uuid) { FortniteServer? findServerById(String uuid) {

View File

@@ -1,51 +1,31 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
class SettingsController extends GetxController { class SettingsController extends GetxController {
static const String storageName = "v3_settings_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String originalDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final RxString language; late final RxString language;
late final Rx<ThemeMode> themeMode; late final Rx<ThemeMode> themeMode;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
late final Rx<UpdateTimer> timer;
late final TextEditingController url;
late final RxBool customGameServer;
late final RxBool firstRun; late final RxBool firstRun;
late final Map<String, Future<bool>> _operations;
late double width; late double width;
late double height; late double height;
late double? offsetX; late double? offsetX;
late double? offsetY; late double? offsetY;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
SettingsController() { SettingsController() {
_storage = appWithNoStorage ? null : GetStorage("settings_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.cobalt);
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
width = _storage?.read("width") ?? kDefaultWindowWidth; width = _storage?.read("width") ?? kDefaultWindowWidth;
height = _storage?.read("height") ?? kDefaultWindowHeight; height = _storage?.read("height") ?? kDefaultWindowHeight;
offsetX = _storage?.read("offset_x"); offsetX = _storage?.read("offset_x");
@@ -54,25 +34,8 @@ class SettingsController extends GetxController {
themeMode.listen((value) => _storage?.write("theme", value.index)); themeMode.listen((value) => _storage?.write("theme", value.index));
language = RxString(_storage?.read("language") ?? currentLocale); language = RxString(_storage?.read("language") ?? currentLocale);
language.listen((value) => _storage?.write("language", value)); language.listen((value) => _storage?.write("language", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage?.write("update_url", url.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true); firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
firstRun.listen((value) => _storage?.write("first_run_tutorial", value)); firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
_operations = {};
}
TextEditingController _createController(String key, InjectableDll dll) {
final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
} }
void saveWindowSize(Size size) { void saveWindowSize(Size size) {
@@ -87,20 +50,6 @@ class SettingsController extends GetxController {
_storage?.write("offset_y", offsetY); _storage?.write("offset_y", offsetY);
} }
void reset(){
gameServerDll.text = _getDefaultPath(InjectableDll.reboot);
unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console);
backendDll.text = _getDefaultPath(InjectableDll.cobalt);
memoryLeakDll.text = _getDefaultPath(InjectableDll.memory);
gameServerPort.text = kDefaultGameServerPort;
timestamp.value = null;
timer.value = UpdateTimer.never;
url.text = kRebootDownloadUrl;
status.value = UpdateStatus.waiting;
customGameServer.value = false;
updateReboot();
}
Future<void> notifyLauncherUpdate() async { Future<void> notifyLauncherUpdate() async {
if (appVersion == null) { if (appVersion == null) {
return; return;
@@ -125,7 +74,8 @@ class SettingsController extends GetxController {
child: Text(translations.updateAvailableAction), child: Text(translations.updateAvailableAction),
onPressed: () { onPressed: () {
infoBar.close(); infoBar.close();
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases")); launchUrl(Uri.parse(
"https://github.com/Auties00/reboot_launcher/releases"));
}, },
) )
); );
@@ -133,7 +83,8 @@ class SettingsController extends GetxController {
Future<dynamic> _getPubspecYaml() async { Future<dynamic> _getPubspecYaml() async {
try { try {
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml")); final pubspecResponse = await http.get(Uri.parse(
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if (pubspecResponse.statusCode != 200) { if (pubspecResponse.statusCode != 200) {
return null; return null;
} }
@@ -144,190 +95,4 @@ class SettingsController extends GetxController {
return null; return null;
} }
} }
Future<bool> updateReboot({bool force = false, bool silent = false}) async {
if(_updater != null) {
return await _updater!;
}
final result = _updateReboot(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateReboot(bool force, bool silent) async {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
return true;
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
infoBarEntry?.close();
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
showRebootInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => updateReboot(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry),
)
);
return false;
}finally {
_updater = null;
}
}
(File, bool) getInjectableData(InjectableDll dll) {
final defaultPath = canonicalize(_getDefaultPath(dll));
switch(dll){
case InjectableDll.reboot:
if(customGameServer.value) {
final file = File(gameServerDll.text);
if(file.existsSync()) {
return (file, true);
}
}
return (rebootDllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.cobalt:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memory:
final memoryLeakFile = File(memoryLeakDll.text);
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
}
}
String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
log("[DLL] Asking for $filePath(silent: $silent)");
final old = _operations[filePath];
if(old != null) {
log("[DLL] Download task already exists");
return old;
}
log("[DLL] Creating new download task...");
final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun;
return newRun;
}
Future<bool> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
InfoBarEntry? entry;
try {
if (fileName == "reboot.dll") {
log("[DLL] Downloading reboot.dll...");
return await updateReboot(
silent: silent
);
}
if(File(filePath).existsSync()) {
log("[DLL] File already exists");
return true;
}
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
entry?.close();
if(!silent) {
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
log("[DLL] Error: $message");
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
}finally {
_operations.remove(fileName);
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
} }

View File

@@ -1,7 +1,7 @@
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog; import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
@@ -126,7 +126,7 @@ class ProgressDialog extends AbstractDialog {
header: InfoLabel( header: InfoLabel(
label: text, label: text,
child: Container( child: Container(
padding: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
width: double.infinity, width: double.infinity,
child: const ProgressBar() child: const ProgressBar()
), ),
@@ -300,7 +300,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _primaryButton => Button( Widget get _primaryButton => Button(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor) backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
), ),
onPressed: widget.onTap!, onPressed: widget.onTap!,
child: Text(widget.text!), child: Text(widget.text!),
@@ -308,7 +308,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _secondaryButton => Button( Widget get _secondaryButton => Button(
style: widget.color != null ? ButtonStyle( style: widget.color != null ? ButtonStyle(
backgroundColor: ButtonState.all(widget.color!) backgroundColor: WidgetStateProperty.all(widget.color!)
) : null, ) : null,
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap, onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
child: Text(widget.text ?? translations.defaultDialogSecondaryAction), child: Text(widget.text ?? translations.defaultDialogSecondaryAction),

View File

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

View File

@@ -1,463 +0,0 @@
import 'dart:async';
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';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:universal_disk_space/universal_disk_space.dart';
import 'package:windows_taskbar/windows_taskbar.dart';
class AddVersionDialog extends StatefulWidget {
final bool closable;
const AddVersionDialog({Key? key, required this.closable}) : super(key: key);
@override
State<AddVersionDialog> createState() => _AddVersionDialogState();
}
class _AddVersionDialogState extends State<AddVersionDialog> {
final GameController _gameController = Get.find<GameController>();
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();
late DiskSpace _diskSpace;
late Future<List<FortniteBuild>> _fetchFuture;
late Future _diskFuture;
Isolate? _isolate;
SendPort? _downloadPort;
Object? _error;
StackTrace? _stackTrace;
@override
void initState() {
_fetchFuture = compute(fetchBuilds, null);
_diskSpace = DiskSpace();
_diskFuture = _diskSpace.scan()
.then((_) => _updateFormDefaults());
super.initState();
}
@override
void dispose() {
_pathController.dispose();
_cancelDownload();
super.dispose();
}
void _cancelDownload() {
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
_downloadPort?.send(kStopBuildDownloadSignal);
_isolate?.kill(priority: Isolate.immediate);
}
@override
Widget build(BuildContext context) => Form(
key: _formKey,
child: Obx(() {
switch(_status.value){
case _DownloadStatus.form:
return FutureBuilder(
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture),
builder: (context, snapshot) {
if (snapshot.hasError) {
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
}
final data = snapshot.data;
if (data == null) {
return ProgressDialog(
text: translations.fetchingBuilds,
showButton: widget.closable,
onStop: () => Navigator.of(context).pop()
);
}
return Obx(() => FormDialog(
content: _buildFormBody(data),
buttons: _formButtons
));
}
);
case _DownloadStatus.downloading:
case _DownloadStatus.extracting:
return GenericDialog(
header: _progressBody,
buttons: _stopButton
);
case _DownloadStatus.error:
return ErrorDialog(
exception: _error ?? Exception(translations.unknownError),
stackTrace: _stackTrace,
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
);
case _DownloadStatus.done:
return InfoDialog(
text: translations.downloadedVersion
);
}
})
);
List<DialogButton> get _formButtons => [
if(widget.closable)
DialogButton(type: ButtonType.secondary),
DialogButton(
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download,
type: widget.closable ? ButtonType.primary : ButtonType.only,
color: FluentTheme.of(context).accentColor,
onTap: () => _startDownload(context),
)
];
void _startDownload(BuildContext context) async {
try {
final topResult = _formKey.currentState?.validate();
if(topResult != true) {
return;
}
final fieldResult = _formFieldKey.currentState?.validate();
if(fieldResult != true) {
return;
}
final build = _build.value;
if(build == null){
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) {
if(message is FortniteBuildDownloadProgress) {
_onProgress(build, message.progress, message.minutesLeft, message.extracting);
}else if(message is SendPort) {
_downloadPort = message;
}else {
_onDownloadError(message, null);
}
});
final options = FortniteBuildDownloadOptions(
build,
Directory(_pathController.text),
communicationPort.sendPort
);
final errorPort = ReceivePort();
errorPort.listen((message) => _onDownloadError(message, null));
_isolate = await Isolate.spawn(
downloadArchiveBuild,
options,
onError: errorPort.sendPort,
errorsAreFatal: true
);
} catch (exception, stackTrace) {
_onDownloadError(exception, stackTrace);
}
}
Future<void> _onDownloadComplete(FortniteBuild build) async {
if (!mounted) {
return;
}
_status.value = _DownloadStatus.done;
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
_addFortniteVersion(build);
}
void _addFortniteVersion(FortniteBuild build) {
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
content: build.version,
location: Directory(_pathController.text)
)));
}
void _onDownloadError(Object? error, StackTrace? stackTrace) {
_cancelDownload();
if (!mounted) {
return;
}
_status.value = _DownloadStatus.error;
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
_error = error;
_stackTrace = stackTrace;
}
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) {
if (!mounted) {
return;
}
if(progress >= 100 && extracting) {
_onDownloadComplete(build);
return;
}
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
if(progress >= 0) {
WindowsTaskbar.setProgress(progress.round(), 100);
}
_timeLeft.value = timeLeft;
_progress.value = progress;
}
Widget get _progressBody {
final timeLeft = _timeLeft.value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting,
style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start,
),
),
const SizedBox(
height: 8.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
translations.buildProgress((_progress.value ?? 0).round()),
style: FluentTheme.maybeOf(context)?.typography.body,
),
if(timeLeft != null)
Text(
translations.timeLeft(timeLeft),
style: FluentTheme.maybeOf(context)?.typography.body,
)
],
),
const SizedBox(
height: 8.0,
),
SizedBox(
width: double.infinity,
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
),
const SizedBox(
height: 8.0,
)
],
);
}
Widget _buildFormBody(List<FortniteBuild> builds) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSourceSelector(),
const SizedBox(
height: 16.0
),
_buildBuildSelector(builds),
FileSelector(
label: translations.gameFolderTitle,
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder,
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
controller: _pathController,
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
folder: true
),
const SizedBox(
height: 16.0
)
],
);
String? _checkGameFolder(text) {
if (text == null || text.isEmpty) {
return translations.emptyGamePath;
}
final directory = Directory(text);
if (!directory.existsSync()) {
return translations.directoryDoesNotExist;
}
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
return translations.missingShippingExe;
}
return null;
}
String? _checkDownloadDestination(text) {
if (text == null || text.isEmpty) {
return translations.invalidDownloadPath;
}
return null;
}
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel(
label: translations.build,
child: FormField<FortniteBuild?>(
key: _formFieldKey,
validator: (data) => _checkBuild(data),
builder: (formContext) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ComboBox<FortniteBuild>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(),
value: _build.value,
onChanged: (value) {
if(value == null){
return;
}
_build.value = value;
formContext.didChange(value);
formContext.validate();
_updateFormDefaults();
}
),
if(formContext.hasError)
const SizedBox(height: 4.0),
if(formContext.hasError)
Text(
formContext.errorText ?? "",
style: TextStyle(
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
),
),
SizedBox(
height: formContext.hasError ? 8.0 : 16.0
),
],
)
)
);
String? _checkBuild(FortniteBuild? data) {
if(data == null) {
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)
);
List<DialogButton> get _stopButton => [
DialogButton(
text: translations.stopLoadingDialogAction,
type: ButtonType.only
)
];
Future<void> _updateFormDefaults() async {
if(_source.value != _BuildSource.local && _build.value?.available != true) {
_build.value = null;
}
if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) {
await _fetchFuture;
final bestDisk = _diskSpace.disks
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
final build = _build.value;
if(build == null){
return;
}
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
_pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
}
_formKey.currentState?.validate();
}
}
enum _DownloadStatus {
form,
downloading,
extracting,
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

@@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4);
const infoBarShortDuration = Duration(seconds: 2); const infoBarShortDuration = Duration(seconds: 2);
const _height = 64.0; const _height = 64.0;
InfoBarEntry showRebootInfoBar(dynamic text, { InfoBarEntry showRebootInfoBar(String text, {
InfoBarSeverity severity = InfoBarSeverity.info, InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false, bool loading = false,
Duration? duration = infoBarShortDuration, Duration? duration = infoBarShortDuration,
@@ -23,19 +23,18 @@ InfoBarEntry showRebootInfoBar(dynamic text, {
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox( Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: _height minHeight: _height
), ),
child: Mica( child: Mica(
elevation: 1, elevation: 1,
child: InfoBar( child: InfoBar(
title: Row( title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if(text is Widget) Expanded(
text, child: text is Widget ? text : Text(text)
if(text is String) ),
Text(text),
if(action != null) if(action != null)
action action
], ],

View File

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

View File

@@ -1,62 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
abstract class RebootPage extends StatefulWidget {
const RebootPage({super.key});
String get name;
String get iconAsset;
RebootPageType get type;
int get index => type.index;
bool hasButton(String? pageName);
@override
RebootPageState createState();
}
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
@override
Widget build(BuildContext context) {
super.build(context);
var buttonWidget = button;
if(buttonWidget == null) {
return _listView;
}
return Column(
children: [
Expanded(
child: _listView,
),
const SizedBox(
height: 8.0,
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 1000
),
child: buttonWidget
)
],
);
}
ListView get _listView => ListView.builder(
itemCount: settings.length,
itemBuilder: (context, index) => settings[index],
);
@override
bool get wantKeepAlive => true;
List<Widget> get settings;
Widget? get button;
}

View File

@@ -1,144 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
class PlayPage extends RebootPage {
const PlayPage({Key? key}) : super(key: key);
@override
RebootPageState<PlayPage> createState() => _PlayPageState();
@override
bool hasButton(String? pageName) => pageName == null;
@override
String get name => translations.playName;
@override
String get iconAsset => "assets/images/play.png";
@override
RebootPageType get type => RebootPageType.play;
}
class _PlayPageState extends RebootPageState<PlayPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final GameController _gameController = Get.find<GameController>();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
Expanded(
child: super.build(context),
)
],
);
}
Widget _buildFirstLaunchInfo() => Obx(() {
if(!_settingsController.firstRun.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text(translations.welcomeTitle),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text(translations.welcomeDescription)
),
action: Button(
child: Text(translations.welcomeAction),
onPressed: () => startOnboarding(),
),
onClose: () => _settingsController.firstRun.value = false
),
),
);
});
@override
Widget? get button => LaunchButton(
startLabel: translations.launchFortnite,
stopLabel: translations.closeFortnite,
host: false
);
@override
List<SettingTile> get settings => [
buildVersionSelector(
key: gameVersionOverlayTargetKey
),
_options,
_internalFiles,
];
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _settingsController.unrealEngineConsoleDll
),
createFileSetting(
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _settingsController.backendDll
),
createFileSetting(
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _settingsController.memoryLeakDll
),
],
);
SettingTile get _options => SettingTile(
icon: Icon(
FluentIcons.options_24_regular
),
title: Text(translations.settingsClientOptionsName),
subtitle: Text(translations.settingsClientOptionsDescription),
children: [
SettingTile(
icon: Icon(
FluentIcons.options_24_regular
),
title: Text(translations.settingsClientArgsName),
subtitle: Text(translations.settingsClientArgsDescription),
content: TextFormBox(
placeholder: translations.settingsClientArgsPlaceholder,
controller: _gameController.customLaunchArgs,
)
)
]
);
}

View File

@@ -1,128 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingsPage extends RebootPage {
const SettingsPage({Key? key}) : super(key: key);
@override
String get name => translations.settingsName;
@override
String get iconAsset => "assets/images/settings.png";
@override
RebootPageType get type => RebootPageType.settings;
@override
bool hasButton(String? pageName) => false;
@override
RebootPageState<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends RebootPageState<SettingsPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
@override
Widget? get button => null;
@override
List<Widget> get settings => [
_language,
_theme,
_resetDefaults,
_installationDirectory
];
SettingTile get _language => SettingTile(
icon: Icon(
FluentIcons.local_language_24_regular
),
title: Text(translations.settingsUtilsLanguageName),
subtitle: Text(translations.settingsUtilsLanguageDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_getLocaleName(_settingsController.language.value)),
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
text: Text(_getLocaleName(locale.languageCode)),
onPressed: () => _settingsController.language.value = locale.languageCode
)).toList()
))
);
String _getLocaleName(String locale) {
var result = LocaleNames.of(context)!.nameOf(locale);
if(result != null) {
return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}";
}
return locale;
}
SettingTile get _theme => SettingTile(
icon: Icon(
FluentIcons.dark_theme_24_regular
),
title: Text(translations.settingsUtilsThemeName),
subtitle: Text(translations.settingsUtilsThemeDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.themeMode.value.title),
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
text: Text(themeMode.title),
onPressed: () => _settingsController.themeMode.value = themeMode
)).toList()
))
);
SettingTile get _resetDefaults => SettingTile(
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.settingsUtilsResetDefaultsName),
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
content: Button(
onPressed: () => showResetDialog(_settingsController.reset),
child: Text(translations.settingsUtilsResetDefaultsContent),
)
);
SettingTile get _installationDirectory => SettingTile(
icon: Icon(
FluentIcons.folder_24_regular
),
title: Text(translations.settingsUtilsInstallationDirectoryName),
subtitle: Text(translations.settingsUtilsInstallationDirectorySubtitle),
content: Button(
onPressed: () => launchUrl(installationDirectory.uri),
child: Text(translations.settingsUtilsInstallationDirectoryContent),
)
);
}
extension _ThemeModeExtension on ThemeMode {
String get title {
switch(this) {
case ThemeMode.system:
return translations.system;
case ThemeMode.dark:
return translations.dark;
case ThemeMode.light:
return translations.light;
}
}
}

113
gui/lib/src/page/page.dart Normal file
View File

@@ -0,0 +1,113 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/widget/message/onboard.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
abstract class RebootPage extends StatefulWidget {
const RebootPage({super.key});
String get name;
String get iconAsset;
RebootPageType get type;
int get index => type.index;
bool hasButton(String? pageName);
@override
RebootPageState createState();
}
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
final SettingsController _settingsController = Get.find<SettingsController>();
@override
Widget build(BuildContext context) {
super.build(context);
var buttonWidget = button;
if(buttonWidget == null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
Expanded(
child: _listView
)
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
Expanded(
child: Column(
children: [
Expanded(
child: _listView,
),
const SizedBox(
height: 8.0,
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 1000
),
child: buttonWidget
)
],
),
),
],
);
}
Widget _buildFirstLaunchInfo() => Obx(() {
if(!_settingsController.firstRun.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text(translations.welcomeTitle),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text(translations.welcomeDescription)
),
action: Button(
child: Text(translations.welcomeAction),
onPressed: () => startOnboarding(),
),
onClose: () => _settingsController.firstRun.value = false
),
),
);
});
ListView get _listView => ListView.builder(
itemCount: settings.length,
itemBuilder: (context, index) => settings[index],
);
@override
bool get wantKeepAlive => true;
List<Widget> get settings;
Widget? get button;
}

View File

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

View File

@@ -1,12 +1,47 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
const Duration _timeout = Duration(seconds: 5); const Duration _timeout = Duration(seconds: 5);
Future<bool> pingGameServer(String address, {Duration? timeout}) async { Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
Future<bool> ping(String hostname, int port) async { final completer = Completer<bool>();
final start = DateTime.now();
_pingGameServerOrTimeout(completer, start, timeout, address);
return completer;
}
Future<void> _pingGameServerOrTimeout(Completer<bool> completer, DateTime start, Duration timeout, String address) async {
while (!completer.isCompleted && max(DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch, 0) < timeout.inMilliseconds) {
final result = await pingGameServer(address);
if(result) {
completer.complete(true);
}else {
await Future.delayed(_timeout);
}
}
if(!completer.isCompleted) {
completer.complete(false);
}
}
Future<bool> pingGameServer(String address) async {
final split = address.split(":");
var hostname = split[0];
if(isLocalHost(hostname)) {
hostname = "127.0.0.1";
}
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
return await _ping(hostname, port)
.timeout(_timeout, onTimeout: () => false);
}
Future<bool> _ping(String hostname, int port) async {
log("[MATCHMAKER] Pinging $hostname:$port"); log("[MATCHMAKER] Pinging $hostname:$port");
RawDatagramSocket? socket; RawDatagramSocket? socket;
try { try {
@@ -35,30 +70,3 @@ Future<bool> pingGameServer(String address, {Duration? timeout}) async {
socket?.close(); socket?.close();
} }
} }
final start = DateTime.now();
var firstTime = true;
final split = address.split(":");
var hostname = split[0];
if(isLocalHost(hostname)) {
hostname = "127.0.0.1";
}
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
final result = await ping(hostname, port)
.timeout(_timeout, onTimeout: () => false);
if(result) {
return true;
}
if(firstTime) {
firstTime = false;
}else {
await Future.delayed(_timeout);
}
}
return false;
}

View File

@@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:win32/win32.dart'; import 'package:win32/win32.dart';
import 'package:window_manager/window_manager.dart';
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))'); final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -24,10 +25,13 @@ bool get isWin11 {
return intBuild != null && intBuild > 22000; return intBuild != null && intBuild > 22000;
} }
Future<String?> openFolderPicker(String title) async => Future<String?> openFolderPicker(String title) async {
await FilePicker.platform.getDirectoryPath(dialogTitle: title); FilePicker.platform = FilePickerWindows();
return await FilePicker.platform.getDirectoryPath(dialogTitle: title);
}
Future<String?> openFilePicker(String extension) async { Future<String?> openFilePicker(String extension) async {
FilePicker.platform = FilePickerWindows();
var result = await FilePicker.platform.pickFiles( var result = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowMultiple: false, allowMultiple: false,
@@ -43,428 +47,60 @@ Future<String?> openFilePicker(String extension) async {
bool get isDarkMode => bool get isDarkMode =>
SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark; SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark;
class _ServiceProvider10 extends IUnknown { extension WindowManagerExtension on WindowManager {
static const String _CLSID = "{C2F03A33-21F5-47FA-B4BB-156362A2F239}"; Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
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 WindowsDisk {
} static final String _nullTerminator = String.fromCharCode(0);
}
class IVirtualDesktop extends IUnknown { final String path;
static const String _CLSID = "{3F07F4BE-B107-441A-AF0F-39D82529072C}"; final int freeBytesAvailable;
final int totalNumberOfBytes;
IVirtualDesktop._internal(super.ptr); const WindowsDisk._internal(this.path, this.freeBytesAvailable, this.totalNumberOfBytes);
String getName() { static List<WindowsDisk> available() {
final result = calloc<HSTRING>(); final buffer = malloc.allocate<Utf16>(MAX_PATH);
final code = (ptr.ref.vtable + 5) try {
.cast< final length = GetLogicalDriveStrings(MAX_PATH, buffer);
Pointer< if (length == 0) {
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<EnumWindowsProc>(_filter, TRUE), result.address);
final length = result.ref.HWndLength;
final HWndsPointer = result.ref.HWnd;
if(HWndsPointer == nullptr) {
calloc.free(result);
return []; return [];
} }
final HWnds = HWndsPointer.asTypedList(length) return buffer.toDartString(length: length)
.toList(growable: false); .split(_nullTerminator)
calloc.free(result); .where((drive) => drive.length > 1)
return HWnds; .map((driveName) {
} final freeBytesAvailable = calloc<Uint64>();
final totalNumberOfBytes = calloc<Uint64>();
class VirtualDesktopManager { final totalNumberOfFreeBytes = calloc<Uint64>();
static VirtualDesktopManager? _instance; try {
GetDiskFreeSpaceEx(
final _IVirtualDesktopManagerInternal windowManager; driveName.toNativeUtf16(),
final _IApplicationViewCollection applicationViewCollection; freeBytesAvailable,
totalNumberOfBytes,
VirtualDesktopManager._internal(this.windowManager, this.applicationViewCollection); totalNumberOfFreeBytes
factory VirtualDesktopManager.getInstance() {
if (_instance != null) {
return _instance!;
}
final hr = CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED | 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
); );
return WindowsDisk._internal(
driveName,
freeBytesAvailable.value,
totalNumberOfBytes.value
);
} finally {
calloc.free(freeBytesAvailable);
calloc.free(totalNumberOfBytes);
calloc.free(totalNumberOfFreeBytes);
}
})
.toList(growable: false);
} finally {
calloc.free(buffer);
}
} }
IVirtualDesktop createDesktop() => windowManager.createDesktop(); @override
String toString() {
void removeDesktop(IVirtualDesktop desktop, [IVirtualDesktop? fallback]) { return 'WindowsDisk{path: $path, freeBytesAvailable: $freeBytesAvailable, totalNumberOfBytes: $totalNumberOfBytes}';
fallback ??= getDesktops().first;
return windowManager.removeDesktop(desktop, fallback);
} }
void setDesktopName(IVirtualDesktop desktop, String newName) =>
windowManager.setDesktopName(desktop, newName);
} }

View File

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

View File

@@ -6,3 +6,14 @@ extension IterableExtension<E> on Iterable<E> {
return null; return null;
} }
} }
extension StringExtension on String {
String? after(String leading) {
final index = indexOf(leading);
if(index == -1) {
return null;
}
return substring(index + leading.length);
}
}

View File

@@ -0,0 +1,63 @@
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
final _hive = HKEY_CURRENT_USER;
void registerUrlProtocol(String scheme, {String? executable, List<String>? arguments}) {
final prefix = _regPrefix(scheme);
final capitalized = scheme[0].toUpperCase() + scheme.substring(1);
final args = _getArguments(arguments).map((a) => _sanitize(a));
final cmd =
'${executable ?? Platform.resolvedExecutable} ${args.join(' ')}';
_regCreateStringKey(_hive, prefix, '', 'URL:$capitalized');
_regCreateStringKey(_hive, prefix, 'URL Protocol', '');
_regCreateStringKey(_hive, prefix + '\\shell\\open\\command', '', cmd);
}
void unregisterUrlProtocol(String scheme) {
final txtKey = TEXT(_regPrefix(scheme));
try {
RegDeleteTree(HKEY_CURRENT_USER, txtKey);
} finally {
free(txtKey);
}
}
String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme';
int _regCreateStringKey(int hKey, String key, String valueName, String data) {
final txtKey = TEXT(key);
final txtValue = TEXT(valueName);
final txtData = TEXT(data);
try {
return RegSetKeyValue(
hKey,
txtKey,
txtValue,
REG_VALUE_TYPE.REG_SZ,
txtData,
txtData.length * 2 + 2,
);
} finally {
free(txtKey);
free(txtValue);
free(txtData);
}
}
String _sanitize(String value) {
value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"');
return '"$value"';
}
List<String> _getArguments(List<String>? arguments) {
if (arguments == null) return ['%s'];
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
throw ArgumentError('arguments must contain at least 1 instance of "%s"');
}
return arguments;
}

View File

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

View File

@@ -0,0 +1,132 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' as fluentIcons show FluentIcons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8;
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(
FluentIcons.document_24_regular
),
title: Text(title),
subtitle: Text(description),
contentWidth: SettingTile.kDefaultContentWidth + _kButtonDimensions,
content: Row(
children: [
Expanded(
child: FileSelector(
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
controller: controller,
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: obx.value == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.selectFile,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _onPressed(selecting, controller),
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
fluentIcons.FluentIcons.open_folder_horizontal
),
)
),
),
)),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: obx.value == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.reset,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: onReset,
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
),
),
))
],
)
);
}
void _onPressed(RxBool selecting, TextEditingController controller) {
if(selecting.value){
return;
}
selecting.value = true;
compute(openFilePicker, "dll")
.then((value) => _updateText(controller, value))
.then((_) => selecting.value = false);
}
void _updateText(TextEditingController controller, String? value) {
final text = value ?? controller.text;
controller.text = text;
controller.selection = TextSelection.collapsed(offset: text.length);
}
String? _checkDll(String? text) {
if (text == null || text.isEmpty) {
return translations.invalidDllPath;
}
final file = File(text);
try {
file.readAsBytesSync();
}catch(_) {
return translations.dllDoesNotExist;
}
if (!text.endsWith(".dll")) {
return translations.invalidDllExtension;
}
return null;
}

View File

@@ -1,41 +0,0 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
icon: Icon(
FluentIcons.document_24_regular
),
title: Text(title),
subtitle: Text(description),
content: FileSelector(
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
controller: controller,
validator: _checkDll,
extension: "dll",
folder: false,
validatorMode: AutovalidateMode.always
)
);
String? _checkDll(String? text) {
if (text == null || text.isEmpty) {
return translations.invalidDllPath;
}
final file = File(text);
if (!file.existsSync()) {
return translations.dllDoesNotExist;
}
if (!text.endsWith(".dll")) {
return translations.invalidDllExtension;
}
return null;
}

View File

@@ -2,8 +2,11 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/profile.dart'; import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/profile.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
class ProfileWidget extends StatefulWidget { class ProfileWidget extends StatefulWidget {
final GlobalKey<OverlayTargetState> overlayKey; final GlobalKey<OverlayTargetState> overlayKey;
@@ -15,6 +18,7 @@ class ProfileWidget extends StatefulWidget {
class _ProfileWidgetState extends State<ProfileWidget> { class _ProfileWidgetState extends State<ProfileWidget> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
@override @override
Widget build(BuildContext context) => OverlayTarget( Widget build(BuildContext context) => OverlayTarget(
@@ -22,7 +26,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
child: HoverButton( child: HoverButton(
margin: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0),
onPressed: () async { onPressed: () async {
if(await showProfileForm(context)) { if(await showProfileForm(context, _username, _password)) {
setState(() {}); setState(() {});
} }
}, },
@@ -57,7 +61,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
_username, _usernameLabel,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w600 fontWeight: FontWeight.w600
@@ -65,7 +69,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
maxLines: 1 maxLines: 1
), ),
Text( Text(
_email, _emailLabel,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w100 fontWeight: FontWeight.w100
@@ -81,8 +85,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
), ),
); );
String get _username { String get _usernameLabel {
var username = _gameController.username.text; final username = _username.text;
if(username.isEmpty) { if(username.isEmpty) {
return kDefaultPlayerName; return kDefaultPlayerName;
} }
@@ -96,8 +100,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
return result.substring(0, 1).toUpperCase() + result.substring(1); return result.substring(0, 1).toUpperCase() + result.substring(1);
} }
String get _email { String get _emailLabel {
var username = _gameController.username.text; final username = _username.text;
if(username.isEmpty) { if(username.isEmpty) {
return "$kDefaultPlayerName@projectreboot.dev"; return "$kDefaultPlayerName@projectreboot.dev";
} }
@@ -108,4 +112,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
return "$username@projectreboot.dev".toLowerCase(); return "$username@projectreboot.dev".toLowerCase();
} }
TextEditingController get _username => pageIndex.value == RebootPageType.host.index ? _hostingController.accountUsername : _gameController.username;
TextEditingController get _password => pageIndex.value == RebootPageType.host.index ? _hostingController.accountPassword : _gameController.password;
} }

View File

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

View File

@@ -7,21 +7,19 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:port_forwarder/port_forwarder.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart';
class LaunchButton extends StatefulWidget { class LaunchButton extends StatefulWidget {
final bool host; final bool host;
@@ -40,12 +38,12 @@ class _LaunchButtonState extends State<LaunchButton> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>(); final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>(); final BackendController _backendController = Get.find<BackendController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final DllController _dllController = Get.find<DllController>();
InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar; InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation; CancelableOperation? _operation;
IVirtualDesktop? _virtualDesktop; Completer? _pingOperation;
@override @override
Widget build(BuildContext context) => Align( Widget build(BuildContext context) => Align(
@@ -73,17 +71,19 @@ class _LaunchButtonState extends State<LaunchButton> {
if (host ? _hostingController.started() : _gameController.started()) { if (host ? _hostingController.started() : _gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance"); log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop( _onStop(
reason: _StopReason.normal reason: _StopReason.normal,
host: host
); );
return; return;
} }
final version = _gameController.selectedVersion; final version = _gameController.selectedVersion.value;
log("[${host ? 'HOST' : 'GAME'}] Version data: $version"); log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
if(version == null){ if(version == null){
log("[${host ? 'HOST' : 'GAME'}] No version selected"); log("[${host ? 'HOST' : 'GAME'}] No version selected");
_onStop( _onStop(
reason: _StopReason.missingVersionError reason: _StopReason.missingVersionError,
host: host
); );
return; return;
} }
@@ -93,62 +93,68 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Set started"); log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) { for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(injectable, host) == null) { if(await _getDllFileOrStop(version.gameVersion, injectable, host) == null) {
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
);
return; return;
} }
} }
try { try {
final executable = await version.shippingExecutable;
if(executable == null){
log("[${host ? 'HOST' : 'GAME'}] No executable found");
_onStop(
reason: _StopReason.missingExecutableError,
error: version.location.path
);
return;
}
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})..."); log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
final backendResult = _backendController.started() || await _backendController.toggleInteractive(); final backendResult = _backendController.started() || await _backendController.toggle();
if(!backendResult){ if(!backendResult){
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend"); log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop( _onStop(
reason: _StopReason.backendError reason: _StopReason.backendError,
host: host
); );
return; return;
} }
log("[${host ? 'HOST' : 'GAME'}] Backend works"); log("[${host ? 'HOST' : 'GAME'}] Backend works");
final serverType = _hostingController.type.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)"); final headless = _hostingController.headless.value;
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false); log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, false);
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance"); log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance); final result = await _startGameProcesses(version, host, headless, linkedHostingInstance);
final started = host ? _hostingController.started() : _gameController.started(); final started = host ? _hostingController.started() : _gameController.started();
if(!started) { if(!started) {
result?.kill(); result?.kill();
return; return;
} }
if(host || linkedHostingInstance != null) {
if (_dllController.gameServerPort.text == kDefaultBackendPort.toString()) {
_onStop(
reason: _StopReason.gameServerPortError,
host: host
);
return;
}
}
if(!host) { if(!host) {
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null); _showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null);
}else { }else {
_showLaunchingGameServerWidget(); _showLaunchingGameServerWidget();
} }
} on ProcessException catch (exception, stackTrace) {
_onStop(
reason: _StopReason.corruptedVersionError,
error: exception.toString(),
stackTrace: stackTrace,
host: host
);
} catch (exception, stackTrace) { } catch (exception, stackTrace) {
_onStop( _onStop(
reason: _StopReason.unknownError, reason: _StopReason.unknownError,
error: exception.toString(), error: exception.toString(),
stackTrace: stackTrace stackTrace: stackTrace,
host: host
); );
} }
} }
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, GameServerType hostType, bool forceLinkedHosting) async { Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool forceLinkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically..."); log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
if(host){ if(host){
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary"); log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
@@ -172,7 +178,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server..."); log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
final instance = await _startGameProcesses(version, true, hostType, null); final instance = await _startGameProcesses(version, true, headless, null);
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server..."); log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
_setStarted(true, true); _setStarted(true, true);
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started"); log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
@@ -182,7 +188,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _askForAutomaticGameServer(bool host) async { Future<bool> _askForAutomaticGameServer(bool host) async {
if (host ? !_hostingController.started() : !_gameController.started()) { if (host ? !_hostingController.started() : !_gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance"); log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop(reason: _StopReason.normal); _onStop(reason: _StopReason.normal, host: host);
return false; return false;
} }
@@ -206,19 +212,10 @@ class _LaunchButtonState extends State<LaunchButton> {
return result; return result;
} }
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameServerType hostType, GameInstance? linkedHosting) async { Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process..."); final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher..."); final eacProcess = await _createPausedProcess(version, host, kEacExe);
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable); final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
log("[${host ? 'HOST' : 'GAME'}] Started paused launcher: $launcherProcess");
log("[${host ? 'HOST' : 'GAME'}] Starting paused eac...");
final eacProcess = await _createPausedProcess(version, version.eacExecutable);
log("[${host ? 'HOST' : 'GAME'}] Started paused eac: $eacProcess");
final executable = await version.shippingExecutable;
log("[${host ? 'HOST' : 'GAME'}] Using game path: ${executable?.path}");
final gameProcess = await _createGameProcess(version, executable!, host, hostType, linkedHosting);
if(gameProcess == null) { if(gameProcess == null) {
log("[${host ? 'HOST' : 'GAME'}] No game process was created"); log("[${host ? 'HOST' : 'GAME'}] No game process was created");
return null; return null;
@@ -226,11 +223,12 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}"); log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance( final instance = GameInstance(
versionName: version.content.toString(), version: version.gameVersion,
host: host,
gamePid: gameProcess, gamePid: gameProcess,
launcherPid: launcherProcess, launcherPid: launcherProcess,
eacPid: eacProcess, eacPid: eacProcess,
serverType: host ? hostType : null, headless: host && headless,
child: linkedHosting child: linkedHosting
); );
if(host){ if(host){
@@ -239,49 +237,95 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{ }else{
_gameController.instance.value = instance; _gameController.instance.value = instance;
} }
await _injectOrShowError(InjectableDll.cobalt, host); await _injectOrShowError(InjectableDll.auth, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance"); log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance; return instance;
} }
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async { Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
try {
log("[${host ? 'HOST' : 'GAME'}] Deleting $kGFSDKAftermathLibDll...");
final dlls = await findFiles(version.location, kGFSDKAftermathLibDll);
log("[${host ? 'HOST' : 'GAME'}] Found ${dlls.length} to delete for $kGFSDKAftermathLibDll");
for(final dll in dlls) {
log("[${host ? 'HOST' : 'GAME'}] Deleting ${dll.path}...");
final result = await delete(dll);
if(result) {
log("[${host ? 'HOST' : 'GAME'}] Deleted ${dll.path}");
}else {
log("[${host ? 'HOST' : 'GAME'}] Cannot delete ${dll.path}");
}
}
}catch(_) {
}
final shippingExecutables = await findFiles(version.location, kShippingExe);
if(shippingExecutables.isEmpty){
log("[${host ? 'HOST' : 'GAME'}] No game executable found");
_onStop(
reason: _StopReason.missingExecutableError,
error: kShippingExe,
host: host
);
return null;
}
if(shippingExecutables.length != 1) {
log("[${host ? 'HOST' : 'GAME'}] Too many game executables found");
_onStop(
reason: _StopReason.multipleExecutablesError,
error: kShippingExe,
host: host
);
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Generating instance args..."); log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs( final gameArgs = createRebootArgs(
_gameController.username.text, host ? _hostingController.accountUsername.text : _gameController.username.text,
_gameController.password.text, host ? _hostingController.accountPassword.text : _gameController.password.text,
host, host,
hostType, headless,
false, false,
"" host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
); );
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}"); log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
final gameProcess = await startProcess( final gameProcess = await startProcess(
executable: executable, executable: shippingExecutables.first,
args: gameArgs, args: gameArgs,
useTempBatch: false, useTempBatch: false,
name: "${version.content}-${host ? 'HOST' : 'GAME'}", name: "${version.gameVersion}-${host ? 'HOST' : 'GAME'}",
environment: { environment: {
"OPENSSL_ia32cap": "~0x20000000" "OPENSSL_ia32cap": "~0x20000000"
} }
); );
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
void onGameOutput(String line, bool error) { void onGameOutput(String line, bool error) {
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line"); log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
handleGameOutput( handleGameOutput(
line: line, line: line,
host: host, host: host,
onShutdown: () => _onStop(reason: _StopReason.normal), onShutdown: () => _onStop(reason: _StopReason.normal, host: host),
onTokenError: () => _onStop(reason: _StopReason.tokenError), onTokenError: () => _onStop(reason: _StopReason.tokenError, host: host),
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError), onBuildCorrupted: () {
if(instance == null) {
return;
}else if(!instance.launched) {
_onStop(reason: _StopReason.corruptedVersionError, host: host);
}else {
_onStop(reason: _StopReason.crash, host: host);
}
},
onLoggedIn: () =>_onLoggedIn(host), onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version), onMatchEnd: () => _onMatchEnd(version)
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
); );
} }
gameProcess.stdOutput.listen((line) => onGameOutput(line, false)); gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
gameProcess.stdError.listen((line) => onGameOutput(line, true)); gameProcess.stdError.listen((line) => onGameOutput(line, true));
gameProcess.exitCode.then((_) async { gameProcess.exitCode.then((_) async {
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
instance?.killed = true;
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal"); log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
_onStop( _onStop(
reason: _StopReason.exitCode, reason: _StopReason.exitCode,
@@ -291,60 +335,37 @@ class _LaunchButtonState extends State<LaunchButton> {
return gameProcess.pid; return gameProcess.pid;
} }
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async { Future<int?> _createPausedProcess(FortniteVersion version, bool host, String executableName) async {
if (file == null) { log("[${host ? 'HOST' : 'GAME'}] Starting $executableName...");
final executables = await findFiles(version.location, executableName);
if(executables.isEmpty){
return null;
}
if(executables.length != 1) {
log("[${host ? 'HOST' : 'GAME'}] Too many $executableName found: $executables");
_onStop(
reason: _StopReason.multipleExecutablesError,
error: executableName,
host: host
);
return null; return null;
} }
final process = await startProcess( final process = await startProcess(
executable: file, executable: executables.first,
useTempBatch: false, useTempBatch: false,
name: "${version.content}-${basenameWithoutExtension(file.path)}", name: "${version.gameVersion}-${basenameWithoutExtension(executables.first.path)}",
environment: { environment: {
"OPENSSL_ia32cap": "~0x20000000" "OPENSSL_ia32cap": "~0x20000000"
} }
); );
log("[${host ? 'HOST' : 'GAME'}] Started paused $executableName: $process");
final pid = process.pid; final pid = process.pid;
suspend(pid); suspend(pid);
return pid; return pid;
} }
Future<void> _onDisplayAttached(bool host, GameServerType type, FortniteVersion version) async {
if(host && type == GameServerType.virtualWindow) {
final hostingInstance = _hostingController.instance.value;
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
hostingInstance.movedToVirtualDesktop = true;
try {
final windowManager = VirtualDesktopManager.getInstance();
_virtualDesktop = windowManager.createDesktop();
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
var success = false;
try {
success = await windowManager.moveWindowToDesktop(
hostingInstance.gamePid,
_virtualDesktop!,
excludedWindowName: "Reboot"
);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
success = false;
}
if(!success) {
try {
windowManager.removeDesktop(_virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}finally {
_virtualDesktop = null;
}
}
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}
}
}
}
void _onMatchEnd(FortniteVersion version) { void _onMatchEnd(FortniteVersion version) {
if(_hostingController.autoRestart.value) { if(_hostingController.autoRestart.value) {
final notification = LocalNotification( final notification = LocalNotification(
@@ -387,21 +408,31 @@ class _LaunchButtonState extends State<LaunchButton> {
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
instance.launched = true; instance.launched = true;
instance.tokenError = false; instance.tokenError = false;
await _injectOrShowError(InjectableDll.memory, host); if(_isChapterOne(instance.version)) {
await _injectOrShowError(InjectableDll.memoryLeak, host);
}
if(!host){ if(!host){
await _injectOrShowError(InjectableDll.console, host); await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected(); _onGameClientInjected();
}else { }else {
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text); final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
if(gameServerPort != null) { if(gameServerPort != null) {
await killProcessByPort(gameServerPort); await killProcessByPort(gameServerPort);
} }
await _injectOrShowError(InjectableDll.reboot, host); await _injectOrShowError(InjectableDll.gameServer, host);
_onGameServerInjected(); _onGameServerInjected();
} }
} }
} }
bool _isChapterOne(String version) {
try {
return Version.parse(version).major < 10;
} on FormatException catch(_) {
return true;
}
}
void _onGameClientInjected() { void _onGameClientInjected() {
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
showRebootInfoBar( showRebootInfoBar(
@@ -418,41 +449,37 @@ class _LaunchButtonState extends State<LaunchButton> {
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
} }
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
try { try {
_gameServerInfoBar = showRebootInfoBar( final gameServerPort = _dllController.gameServerPort.text;
translations.waitingForGameServer, final started = await _checkLocalGameServer(gameServerPort);
loading: true, if(!started) {
duration: null if (_hostingController.instance.value?.killed != true) {
);
final gameServerPort = _settingsController.gameServerPort.text;
final localPingResult = await pingGameServer(
"127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 2)
);
_gameServerInfoBar?.close();
if (!localPingResult) {
showRebootInfoBar( showRebootInfoBar(
translations.gameServerStartWarning, translations.gameServerStartWarning,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
}
return; return;
} }
_backendController.joinLocalhost(); _backendController.joinLocalhost();
final accessible = await _checkGameServer(theme, gameServerPort); final accessible = await _checkPublicGameServer(gameServerPort);
if (!accessible) { if (!accessible) {
showRebootInfoBar( showRebootInfoBar(
translations.gameServerStartLocalWarning, translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning, severity: InfoBarSeverity.warning,
duration: infoBarLongDuration duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
child: Text(translations.checkGameServerFixAction),
),
); );
return; return;
} }
await _hostingController.publishServer( await _hostingController.publishServer(
_gameController.username.text, _hostingController.accountUsername.text,
_hostingController.instance.value!.versionName, _hostingController.instance.value!.version.toString(),
); );
showRebootInfoBar( showRebootInfoBar(
translations.gameServerStarted, translations.gameServerStarted,
@@ -464,7 +491,30 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async { Future<bool> _checkLocalGameServer(String gameServerPort) async {
try {
_gameServerInfoBar = showRebootInfoBar(
translations.waitingForGameServer,
loading: true,
duration: null
);
final gameServerPort = _dllController.gameServerPort.text;
final pingOperation = pingGameServerOrTimeout(
"127.0.0.1:$gameServerPort",
const Duration(minutes: 2)
);
this._pingOperation = pingOperation;
final localPingResult = await pingOperation.future;
_gameServerInfoBar?.close();
return localPingResult;
}catch(_) {
_gameServerInfoBar?.close();
return false;
}
}
Future<bool> _checkPublicGameServer(String gameServerPort) async {
try { try {
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = showRebootInfoBar(
translations.checkingGameServer, translations.checkingGameServer,
@@ -472,77 +522,105 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
final publicIp = await Ipify.ipv4(); final publicIp = await Ipify.ipv4();
final externalResult = await pingGameServer("$publicIp:$gameServerPort"); var pingOperation = await pingGameServerOrTimeout(
if (externalResult) { "$publicIp:$gameServerPort",
const Duration(seconds: 10)
);
_pingOperation = pingOperation;
var publicPingResult = await pingOperation.future;
if (publicPingResult) {
_gameServerInfoBar?.close();
return true; return true;
} }
final gateway = await Gateway.discover();
if (gateway == null) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
final future = pingGameServer( return false;
}
final forwarded = await gateway.openPort(
protocol: PortType.udp,
externalPort: int.parse(gameServerPort),
portDescription: "Reboot Game Server"
);
if (!forwarded) {
_gameServerInfoBar?.close();
return false;
}
// Give the modem a couple of seconds just in case
// This is not technically necessary, but I can't guarantee that the modem has no race conditions
// So might as well wait
await Future.delayed(const Duration(seconds: 5));
pingOperation = await pingGameServerOrTimeout(
"$publicIp:$gameServerPort", "$publicIp:$gameServerPort",
timeout: const Duration(days: 365) const Duration(seconds: 10)
); );
_gameServerInfoBar = showRebootInfoBar( _pingOperation = pingOperation;
translations.checkGameServerFixMessage(gameServerPort), publicPingResult = await pingOperation.future;
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
);
return await future;
}finally {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return publicPingResult;
}catch(_) {
_gameServerInfoBar?.close();
return false;
}
}
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 have been already terminated, don't bother checking
} finally {
_pingOperation = null;
} }
} }
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
if(host == null) {
await _operation?.cancel(); await _operation?.cancel();
_operation = null; _operation = null;
}
host = host ?? widget.host;
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){ if(host){
_hostingController.instance.value = null; _hostingController.instance.value = null;
}else { }else {
_gameController.instance.value = null; _gameController.instance.value = null;
} }
if(_virtualDesktop != null) {
try {
final instance = VirtualDesktopManager.getInstance();
instance.removeDesktop(_virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] Cannot close virtual desktop: $error");
}
}
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace"); log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}"); log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
if(host) { if(host) {
_hostingController.discardServer(); _hostingController.discardServer();
} }
if(instance != null) {
if(reason == _StopReason.normal) { if(reason == _StopReason.normal) {
instance.launched = true; instance?.launched = true;
} }
instance.kill(); instance?.kill();
final child = instance.child; final child = instance?.child;
if(child != null) { if(child != null) {
await _onStop( await _onStop(
reason: reason, reason: reason,
host: child.serverType != null host: child.host,
error: error,
stackTrace: stackTrace,
interactive: false
); );
} }
}
_setStarted(host, false); _setStarted(host, false);
if(interactive) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) { if(host == true) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
@@ -570,20 +648,33 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.multipleExecutablesError:
showRebootInfoBar(
translations.multipleExecutablesError(error ?? translations.unknown),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.exitCode: case _StopReason.exitCode:
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
final injectedDlls = instance.injectedDlls;
showRebootInfoBar( showRebootInfoBar(
translations.corruptedVersionError, translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
} }
break; break;
case _StopReason.corruptedVersionError: case _StopReason.corruptedVersionError:
final injectedDlls = instance?.injectedDlls ?? [];
showRebootInfoBar( showRebootInfoBar(
translations.corruptedVersionError, translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
); );
break; break;
case _StopReason.corruptedDllError: case _StopReason.corruptedDllError:
@@ -601,8 +692,10 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.tokenError: case _StopReason.tokenError:
_backendController.stop(interactive: false);
final injectedDlls = instance?.injectedDlls;
showRebootInfoBar( showRebootInfoBar(
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none), translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button( action: Button(
@@ -611,6 +704,13 @@ class _LaunchButtonState extends State<LaunchButton> {
) )
); );
break; break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? translations.gameServer : translations.client),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError: case _StopReason.unknownError:
showRebootInfoBar( showRebootInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError), translations.unknownFortniteError(error ?? translations.unknownError),
@@ -618,6 +718,14 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
break; break;
case _StopReason.gameServerPortError:
showRebootInfoBar(
translations.gameServerPortEqualsBackendPort(kDefaultBackendPort),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
}
} }
} }
@@ -631,19 +739,15 @@ class _LaunchButtonState extends State<LaunchButton> {
try { try {
final gameProcess = instance.gamePid; final gameProcess = instance.gamePid;
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess"); log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
final dllPath = await _getDllFileOrStop(injectable, hosting); final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath"); log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
if (dllPath == null) { if (dllPath == null) {
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist"); log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: hosting
);
return; return;
} }
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}..."); log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable
.name}...");
await injectDll(gameProcess, dllPath); await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable); instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}"); log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
@@ -658,25 +762,38 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async { Future<File?> _getDllFileOrStop(String version, InjectableDll injectable, bool host) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}..."); log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final (file, customDll) = _settingsController.getInjectableData(injectable); final (file, customDll) = _dllController.getInjectableData(version, injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll"); log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) { try {
await file.readAsBytes();
log("[${host ? 'HOST' : 'GAME'}] Path exists"); log("[${host ? 'HOST' : 'GAME'}] Path exists");
return file; return file;
}catch(_) {
} }
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist"); log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
if(customDll || isRetry) { if(customDll) {
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery"); log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: host
);
return null; return null;
} }
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again..."); log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _settingsController.downloadCriticalDllInteractive(file.path); final result = await _dllController.download(injectable, file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); if(result) {
return _getDllFileOrStop(injectable, host, true); log("[${host ? 'HOST' : 'GAME'}] Downloaded critical dll");
return file;
}
_onStop(reason: _StopReason.normal, host: host);
return null;
} }
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar( InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
@@ -685,7 +802,7 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) { InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar( return _gameClientInfoBar = showRebootInfoBar(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly, linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true, loading: true,
@@ -703,9 +820,9 @@ class _LaunchButtonState extends State<LaunchButton> {
onPressed: () async { onPressed: () async {
_backendController.joinLocalhost(); _backendController.joinLocalhost();
if(!_hostingController.started.value) { if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true); _gameController.instance.value?.child = await _startMatchMakingServer(version, false, headless, true);
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
_showLaunchingGameClientWidget(version, hostType, true); _showLaunchingGameClientWidget(version, headless, true);
} }
}, },
child: Text(translations.startGameServer), child: Text(translations.startGameServer),
@@ -720,6 +837,7 @@ enum _StopReason {
normal, normal,
missingVersionError, missingVersionError,
missingExecutableError, missingExecutableError,
multipleExecutablesError,
corruptedVersionError, corruptedVersionError,
missingCustomDllError, missingCustomDllError,
corruptedDllError, corruptedDllError,
@@ -727,7 +845,9 @@ enum _StopReason {
matchmakerError, matchmakerError,
tokenError, tokenError,
unknownError, unknownError,
exitCode; gameServerPortError,
exitCode,
crash;
bool get isError => name.contains("Error"); bool get isError => name.contains("Error");
} }

View File

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

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