Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c6cd6dd37 | ||
|
|
c3ede3b745 | ||
|
|
d2f0d176eb | ||
|
|
f9cf99a6b2 | ||
|
|
dc2d4c4377 | ||
|
|
5d8f6bf0fa | ||
|
|
9a000db3b7 | ||
|
|
4327541ac6 | ||
|
|
64dc971da4 | ||
|
|
d36da909ed | ||
|
|
90448eeaa1 | ||
|
|
b319479def | ||
|
|
d5e41ed646 | ||
|
|
9e20ec86e6 | ||
|
|
004fc41292 | ||
|
|
ee466df630 | ||
|
|
fdb1d694d9 | ||
|
|
0cfa4af236 | ||
|
|
d42946c44b | ||
|
|
0a59a32c1b | ||
|
|
2046cb14f6 | ||
|
|
e3f7a1d2cc | ||
|
|
cd6752ed3f | ||
|
|
e1df46efd9 | ||
|
|
dccd05e57f | ||
|
|
eb7745cc4d | ||
|
|
7d5e17642a | ||
|
|
6f91ad0404 | ||
|
|
0c38528e77 | ||
|
|
dfebe74518 | ||
|
|
bfe15e43d9 | ||
|
|
62dae468bf | ||
|
|
a9af28273a | ||
|
|
232bf8fbfc | ||
|
|
a787c4efc9 | ||
|
|
4c3fe9bc65 | ||
|
|
3f88d5ed80 | ||
|
|
582270849e | ||
|
|
1ef4e76768 | ||
|
|
cd8c8e6dd9 | ||
|
|
170a878e79 | ||
|
|
a2505011d9 | ||
|
|
3e2c2e96b1 |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
backend/**/* linguist-vendored
|
||||||
17
README.md
@@ -1,14 +1,25 @@
|
|||||||

|

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

|
||||||
|
|
||||||
|
- CLI
|
||||||
|
|
||||||
|
Coming soon!
|
||||||
|
|||||||
2
backend/index.js
vendored
@@ -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);
|
||||||
|
|
||||||
|
|||||||
776
backend/responses/keychain.json
vendored
29
backend/structure/mcp.js
vendored
@@ -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`);
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
0
cli/lib/src/command/backend.dart
Normal file
0
cli/lib/src/command/commands.dart
Normal file
0
cli/lib/src/command/config.dart
Normal file
0
cli/lib/src/command/host.dart
Normal file
0
cli/lib/src/command/play.dart
Normal file
0
cli/lib/src/command/versions.dart
Normal file
34
cli/lib/src/controller/config.dart
Normal 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');
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:process_run/process_run.dart';
|
|
||||||
import 'package:reboot_cli/cli.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
|
|
||||||
Process? _gameProcess;
|
|
||||||
Process? _launcherProcess;
|
|
||||||
Process? _eacProcess;
|
|
||||||
|
|
||||||
Future<void> startGame() async {
|
|
||||||
await _startLauncherProcess(version);
|
|
||||||
await _startEacProcess(version);
|
|
||||||
|
|
||||||
var executable = await version.shippingExecutable;
|
|
||||||
if (executable == null) {
|
|
||||||
throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username == null) {
|
|
||||||
username = "Reboot${host ? 'Host' : 'Player'}";
|
|
||||||
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
|
|
||||||
..exitCode.then((_) => _onClose())
|
|
||||||
..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
|
|
||||||
_injectOrShowError("cobalt.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
|
||||||
if (dummyVersion.launcherExecutable == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_launcherProcess = await Process.start(dummyVersion.launcherExecutable!.path, []);
|
|
||||||
suspend(_launcherProcess!.pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
|
||||||
if (dummyVersion.eacExecutable == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
|
||||||
suspend(_eacProcess!.pid);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
|
||||||
if(verbose) {
|
|
||||||
stdout.writeln(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGameOutput(
|
|
||||||
line: line,
|
|
||||||
host: hosting,
|
|
||||||
onDisplayAttached: () {}, // TODO: Support virtual desktops
|
|
||||||
onLoggedIn: onLoggedIn,
|
|
||||||
onMatchEnd: onMatchEnd,
|
|
||||||
onShutdown: onShutdown,
|
|
||||||
onTokenError: onTokenError,
|
|
||||||
onBuildCorrupted: onBuildCorrupted
|
|
||||||
);
|
|
||||||
|
|
||||||
if (line.contains(kShutdownLine)) {
|
|
||||||
_onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(kCannotConnectErrors.any((element) => line.contains(element))){
|
|
||||||
stderr.writeln("The backend doesn't work! Token expired");
|
|
||||||
_onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(line.contains("Region ")){
|
|
||||||
if(hosting) {
|
|
||||||
_injectOrShowError(dll, false);
|
|
||||||
}else {
|
|
||||||
_injectOrShowError("console.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
_injectOrShowError("memory.dll");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _kill() {
|
|
||||||
_gameProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
_launcherProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
_eacProcess?.kill(ProcessSignal.sigabrt);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
|
|
||||||
if (_gameProcess == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
stdout.writeln("Injecting $binary...");
|
|
||||||
var dll = locate ? File("${dllsDirectory.path}\\$binary") : File(binary);
|
|
||||||
if(!dll.existsSync()){
|
|
||||||
throw Exception("Cannot inject $dll: missing file");
|
|
||||||
}
|
|
||||||
|
|
||||||
await injectDll(_gameProcess!.pid, dll);
|
|
||||||
} catch (exception) {
|
|
||||||
throw Exception("Cannot inject binary: $binary");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onClose() {
|
|
||||||
_kill();
|
|
||||||
sleep(const Duration(seconds: 3));
|
|
||||||
stdout.writeln("The game was closed");
|
|
||||||
if(autoRestart){
|
|
||||||
stdout.writeln("Restarting automatically game");
|
|
||||||
startGame();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
|
|
||||||
// TODO: Use github
|
|
||||||
const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
|
|
||||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
|
|
||||||
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memory.dll";
|
|
||||||
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
|
|
||||||
|
|
||||||
Future<void> downloadRequiredDLLs() async {
|
|
||||||
stdout.writeln("Downloading necessary components...");
|
|
||||||
var consoleDll = File("${dllsDirectory.path}\\console.dll");
|
|
||||||
if(!consoleDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_consoleDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download console.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await consoleDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var craniumDll = File("${dllsDirectory.path}\\cobalt.dll");
|
|
||||||
if(!craniumDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_baseDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download cobalt.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await craniumDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var memoryFixDll = File("${dllsDirectory.path}\\memory.dll");
|
|
||||||
if(!memoryFixDll.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_memoryFixDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download memory.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
await memoryFixDll.writeAsBytes(response.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!backendDirectory.existsSync()){
|
|
||||||
var response = await http.get(Uri.parse(_embeddedConfigDownload));
|
|
||||||
if(response.statusCode != 200){
|
|
||||||
throw Exception("Cannot download embedded server config");
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
|
||||||
await tempZip.writeAsBytes(response.bodyBytes);
|
|
||||||
await extractFileToDisk(tempZip.path, backendDirectory.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_common/src/util/backend.dart' as server;
|
|
||||||
|
|
||||||
Future<bool> startServerCli(String? host, int? port, ServerType type) async {
|
|
||||||
stdout.writeln("Starting backend server...");
|
|
||||||
switch(type){
|
|
||||||
case ServerType.local:
|
|
||||||
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(_){
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
160
cli/lib/src/util/console.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
cli/lib/src/util/extensions.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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");
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
@@ -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,20 +35,16 @@ class GameInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _kill() {
|
void _kill() {
|
||||||
|
if(!killed) {
|
||||||
launched = true;
|
launched = true;
|
||||||
killed = true;
|
killed = true;
|
||||||
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
||||||
if(launcherPid != null) {
|
if (launcherPid != null) {
|
||||||
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
|
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
|
||||||
}
|
}
|
||||||
if(eacPid != null) {
|
if (eacPid != null) {
|
||||||
Process.killPid(eacPid!, ProcessSignal.sigabrt);
|
Process.killPid(eacPid!, ProcessSignal.sigabrt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GameServerType {
|
|
||||||
headless,
|
|
||||||
virtualWindow,
|
|
||||||
window
|
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -52,7 +196,7 @@ Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
|||||||
await request.close().timeout(const Duration(seconds: 10));
|
await request.close().timeout(const Duration(seconds: 10));
|
||||||
log("[BACKEND] Ping successful");
|
log("[BACKEND] Ping successful");
|
||||||
return uri;
|
return uri;
|
||||||
}catch(error){
|
}catch(error) {
|
||||||
log("[BACKEND] Cannot ping backend: $error");
|
log("[BACKEND] Cannot ping backend: $error");
|
||||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
560
common/lib/src/util/downloader.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
383
common/lib/src/util/game.dart
Normal 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;
|
||||||
|
});
|
||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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());
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 398 KiB After Width: | Height: | Size: 0 B |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 0 B |
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -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": [],
|
||||||
|
|||||||
BIN
gui/assets/build/aria2c.exe
Normal file
@@ -1,2 +0,0 @@
|
|||||||
taskkill /f /im winrar.exe
|
|
||||||
taskkill /f /im tar.exe
|
|
||||||
@@ -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
|
|
||||||
BIN
gui/dependencies/dlls/RebootFallback.zip
Normal file
BIN
gui/dependencies/dlls/RebootS20Fallback.zip
Normal file
BIN
gui/dependencies/dlls/starfall.dll
Normal file
BIN
gui/dependencies/redist/VC_redist.x64.exe
Normal 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
378
gui/lib/src/controller/dll_controller.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,32 +50,18 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pubspec = await _getPubspecYaml();
|
final pubspec = await _getPubspecYaml();
|
||||||
if(pubspec == null) {
|
if (pubspec == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final latestVersion = Version.parse(pubspec["version"]);
|
final latestVersion = Version.parse(pubspec["version"]);
|
||||||
if(latestVersion <= appVersion) {
|
if (latestVersion <= appVersion) {
|
||||||
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,201 +83,16 @@ 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(
|
||||||
if(pubspecResponse.statusCode != 200) {
|
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||||
|
if (pubspecResponse.statusCode != 200) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return loadYaml(pubspecResponse.body);
|
return loadYaml(pubspecResponse.body);
|
||||||
}catch(error) {
|
} catch (error) {
|
||||||
log("[UPDATER] Cannot check for updates: $error");
|
log("[UPDATER] Cannot check for updates: $error");
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
@@ -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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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
|
||||||
],
|
],
|
||||||
@@ -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,
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -34,31 +69,4 @@ Future<bool> pingGameServer(String address, {Duration? timeout}) async {
|
|||||||
}finally {
|
}finally {
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
@@ -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 IVirtualDesktop extends IUnknown {
|
class WindowsDisk {
|
||||||
static const String _CLSID = "{3F07F4BE-B107-441A-AF0F-39D82529072C}";
|
static final String _nullTerminator = String.fromCharCode(0);
|
||||||
|
|
||||||
IVirtualDesktop._internal(super.ptr);
|
final String path;
|
||||||
|
final int freeBytesAvailable;
|
||||||
|
final int totalNumberOfBytes;
|
||||||
|
|
||||||
String getName() {
|
const WindowsDisk._internal(this.path, this.freeBytesAvailable, this.totalNumberOfBytes);
|
||||||
final result = calloc<HSTRING>();
|
|
||||||
final code = (ptr.ref.vtable + 5)
|
|
||||||
.cast<
|
|
||||||
Pointer<
|
|
||||||
NativeFunction<HRESULT Function(Pointer, Pointer<HSTRING>)>>>()
|
|
||||||
.value
|
|
||||||
.asFunction<
|
|
||||||
int Function(Pointer, Pointer<HSTRING>)>()(ptr.ref.lpVtbl, result);
|
|
||||||
if (code != 0) {
|
|
||||||
free(result);
|
|
||||||
throw WindowsException(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertFromHString(result.value);
|
static List<WindowsDisk> available() {
|
||||||
}
|
final buffer = malloc.allocate<Utf16>(MAX_PATH);
|
||||||
}
|
try {
|
||||||
|
final length = GetLogicalDriveStrings(MAX_PATH, buffer);
|
||||||
class IApplicationView extends IUnknown {
|
if (length == 0) {
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
gui/lib/src/util/url_protocol.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
132
gui/lib/src/widget/file/file_setting_tile.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||