32 Commits

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

View File

@@ -1,14 +1,16 @@
![Banner](https://i.imgur.com/p0P4tcI.png)
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
Join our discord at https://discord.gg/reboot
Join our [Discord](https://discord.gg/rebootmp)
## Modules
- COMMON: Shared business logic for CLI and GUI modules
- CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart
- GUI: Stable graphical user interface to play and host Fortnite S0-14
![image](https://github.com/user-attachments/assets/7ff5d49e-8920-41ad-a805-188d84ad6ec4)
## Installation
Check the releases section
Check the releases section

2
backend/index.js vendored
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,87 +1,89 @@
import 'dart:io';
import 'dart:collection';
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';
class Parser {
final List<Command> commands;
late String? username;
late bool host;
late bool verbose;
late String dll;
late FortniteVersion version;
late bool autoRestart;
Parser({required this.commands});
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");
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;
}
stdout.writeln("Launching game...");
var executable = version.shippingExecutable;
if(executable == null){
throw Exception("Missing game executable at: ${version.location.path}");
}
Set<String> _toParameters(Command? parent) => parent?.parameters
.map((e) => '--${e.toLowerCase()}')
.toSet() ?? {};
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"])
Map<String, Command> _toMap(List<Command> children) => Map.fromIterable(
children,
key: (command) => command.name.toLowerCase(),
value: (command) => command
);
if(!started){
stderr.writeln("Cannot start server!");
return;
}
}
writeMatchmakingIp(result["matchmaking-address"]);
autoRestart = result["auto-restart"];
await startGame();
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}';
}

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

@@ -0,0 +1,102 @@
import 'package:interact/interact.dart';
import 'package:reboot_cli/cli.dart';
import 'package:tint/tint.dart';
const Command _buildImport = Command(name: 'import', parameters: ['version', 'path'], subCommands: []);
const Command _buildDownload = Command(name: 'download', parameters: ['version', 'path'], subCommands: []);
const Command _build = Command(name: 'build', parameters: [], subCommands: [_buildImport, _buildDownload]);
const Command _play = Command(name: 'play', parameters: [], subCommands: []);
const Command _host = Command(name: 'host', parameters: [], subCommands: []);
const Command _backend = Command(name: 'backend', parameters: [], subCommands: []);
void main(List<String> args) {
_welcome();
final parser = Parser(commands: [_build, _play, _host, _backend]);
final command = parser.parse(args);
print(command);
_handleRootCommand(command);
}
void _handleRootCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
case 'build':
_handleBuildCommand(call?.subCall);
break;
case 'play':
_handleBuildCommand(call?.subCall);
break;
case 'host':
_handleBuildCommand(call?.subCall);
break;
case 'backend':
_handleBuildCommand(call?.subCall);
break;
default:
_askRootCommand();
break;
}
}
void _askRootCommand() {
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: '')
);
_handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
}
void _handleBuildCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
case 'import':
_handleBuildImportCommand(call!);
break;
case 'download':
_handleBuildDownloadCommand(call!);
break;
default:
_askBuildCommand();
break;
}
}
void _handleBuildImportCommand(CommandCall call) {
final version = call.parameters['path'];
final path = call.parameters['path'];
print(version);
print(path);
}
void _handleBuildDownloadCommand(CommandCall call) {
}
void _askBuildCommand() {
final commands = [_buildImport.name, _buildDownload.name];
final commandSelector = Select.withTheme(
prompt: ' Select a build command:',
options: commands,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '')
);
_handleBuildCommand(CommandCall(name: commands[commandSelector.interact()]));
}
void _handlePlayCommand(CommandCall? call) {
}
void _handleHostCommand(CommandCall? call) {
}
void _handleBackendCommand(CommandCall? call) {
}
void _welcome() => print("""
🎮 Reboot Launcher
🔥 Launch, manage, and play Fortnite using Project Reboot!
🚀 Developed by Auties00 - Version 10.0.7
""".green());

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
const String kDefaultPlayerName = "Player";
const String kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777";
const String kInitializedLine = "Game Engine Initialized";
@@ -11,7 +12,7 @@ const List<String> kCorruptedBuildErrors = [
"Critical error",
"when 0 bytes remain",
"Pak chunk signature verification failed!",
"Couldn't find pak signature file"
"LogWindows:Error: Fatal error!"
];
const List<String> kCannotConnectErrors = [
"port 3551 failed: Connection refused",
@@ -21,4 +22,5 @@ const List<String> kCannotConnectErrors = [
"UOnlineAccountCommon::ForceLogout"
];
const String kGameFinishedLine = "PlayersLeft: 1";
const String kDisplayInitializedLine = "Display";
const String kDisplayLine = "Display";
const String kDisplayInitializedLine = "Initialized";

View File

@@ -9,9 +9,22 @@ extension FortniteVersionExtension on FortniteVersion {
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);
for(final child in directory.listSync()) {
if(child is Directory) {
if(!path.basename(child.path).startsWith("\.")) {
final result = findFile(child, name);
if(result != null) {
return result;
}
}
}else if(child is File) {
if(path.basename(child.path) == name) {
return child;
}
}
}
return null;
}catch(_){
return null;
}

View File

@@ -1,6 +1,10 @@
enum InjectableDll {
console,
cobalt,
reboot,
memory
auth,
gameServer,
memoryLeak
}
extension InjectableDllVersionAware on InjectableDll {
bool get isVersionDependent => this == InjectableDll.gameServer;
}

View File

@@ -17,13 +17,15 @@ class FortniteBuild {
class FortniteBuildDownloadProgress {
final double progress;
final int? minutesLeft;
final int? timeLeft;
final bool extracting;
final int speed;
FortniteBuildDownloadProgress({
required this.progress,
required this.extracting,
this.minutesLeft,
required this.timeLeft,
required this.speed
});
}

View File

@@ -1,10 +1,11 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:version/version.dart';
class GameInstance {
final String versionName;
final Version version;
final int gamePid;
final int? launcherPid;
final int? eacPid;
@@ -17,7 +18,7 @@ class GameInstance {
GameInstance? child;
GameInstance({
required this.versionName,
required this.version,
required this.gamePid,
required this.launcherPid,
required this.eacPid,

View File

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

View File

@@ -15,10 +15,137 @@ final Semaphore _semaphore = Semaphore();
String? _lastIp;
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,
window: detached,
);
);
process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
process.stdError.listen((error) {
log("[BACKEND] Error: $error");
onError?.call(error);
});
if(!detached) {
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
}
return process;
}
Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);
@@ -52,7 +179,7 @@ Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
await request.close().timeout(const Duration(seconds: 10));
log("[BACKEND] Ping successful");
return uri;
}catch(error){
}catch(error) {
log("[BACKEND] Cannot ping backend: $error");
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
}

View File

@@ -3,165 +3,311 @@ 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:uuid/uuid.dart';
import 'package:version/version.dart';
import 'package:http/http.dart' as http;
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 int _ariaPort = 6800;
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
final String _ariaSecret = "RebootLauncher";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
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;
}
final List<FortniteBuild> downloadableBuilds = [
FortniteBuild(version: Version.parse("1.7.2"), link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
FortniteBuild(version: Version.parse("1.8"), link: "https://public.simplyblk.xyz/1.8.rar", available: true),
FortniteBuild(version: Version.parse("1.8.1"), link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
FortniteBuild(version: Version.parse("1.8.2"), link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
FortniteBuild(version: Version.parse("1.9"), link: "https://public.simplyblk.xyz/1.9.rar", available: true),
FortniteBuild(version: Version.parse("1.9.1"), link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
FortniteBuild(version: Version.parse("1.10"), link: "https://public.simplyblk.xyz/1.10.rar", available: true),
FortniteBuild(version: Version.parse("1.11"), link: "https://public.simplyblk.xyz/1.11.zip", available: true),
FortniteBuild(version: Version.parse("2.1.0"), link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
FortniteBuild(version: Version.parse("2.2.0"), link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
FortniteBuild(version: Version.parse("2.3"), link: "https://public.simplyblk.xyz/2.3.rar", available: true),
FortniteBuild(version: Version.parse("2.4.0"), link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
FortniteBuild(version: Version.parse("2.4.2"), link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
FortniteBuild(version: Version.parse("2.5.0"), link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
FortniteBuild(version: Version.parse("3.0"), link: "https://public.simplyblk.xyz/3.0.zip", available: true),
FortniteBuild(version: Version.parse("3.1"), link: "https://public.simplyblk.xyz/3.1.rar", available: true),
FortniteBuild(version: Version.parse("3.1.1"), link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
FortniteBuild(version: Version.parse("3.2"), link: "https://public.simplyblk.xyz/3.2.zip", available: true),
FortniteBuild(version: Version.parse("3.3"), link: "https://public.simplyblk.xyz/3.3.rar", available: true),
FortniteBuild(version: Version.parse("3.5"), link: "https://public.simplyblk.xyz/3.5.rar", available: true),
FortniteBuild(version: Version.parse("3.6"), link: "https://public.simplyblk.xyz/3.6.zip", available: true),
FortniteBuild(version: Version.parse("4.0"), link: "https://public.simplyblk.xyz/4.0.zip", available: true),
FortniteBuild(version: Version.parse("4.1"), link: "https://public.simplyblk.xyz/4.1.zip", available: true),
FortniteBuild(version: Version.parse("4.2"), link: "https://public.simplyblk.xyz/4.2.zip", available: true),
FortniteBuild(version: Version.parse("4.4"), link: "https://public.simplyblk.xyz/4.4.rar", available: true),
FortniteBuild(version: Version.parse("4.5"), link: "https://public.simplyblk.xyz/4.5.rar", available: true),
FortniteBuild(version: Version.parse("5.00"), link: "https://public.simplyblk.xyz/5.00.rar", available: true),
FortniteBuild(version: Version.parse("5.0.1"), link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
FortniteBuild(version: Version.parse("5.10"), link: "https://public.simplyblk.xyz/5.10.rar", available: true),
FortniteBuild(version: Version.parse("5.21"), link: "https://public.simplyblk.xyz/5.21.rar", available: true),
FortniteBuild(version: Version.parse("5.30"), link: "https://public.simplyblk.xyz/5.30.rar", available: true),
FortniteBuild(version: Version.parse("5.40"), link: "https://public.simplyblk.xyz/5.40.rar", available: true),
FortniteBuild(version: Version.parse("6.00"), link: "https://public.simplyblk.xyz/6.00.rar", available: true),
FortniteBuild(version: Version.parse("6.01"), link: "https://public.simplyblk.xyz/6.01.rar", available: true),
FortniteBuild(version: Version.parse("6.1.1"), link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
FortniteBuild(version: Version.parse("6.02"), link: "https://public.simplyblk.xyz/6.02.rar", available: true),
FortniteBuild(version: Version.parse("6.2.1"), link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10"), link: "https://public.simplyblk.xyz/6.10.rar", available: true),
FortniteBuild(version: Version.parse("6.10.1"), link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10.2"), link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
FortniteBuild(version: Version.parse("6.21"), link: "https://public.simplyblk.xyz/6.21.rar", available: true),
FortniteBuild(version: Version.parse("6.22"), link: "https://public.simplyblk.xyz/6.22.rar", available: true),
FortniteBuild(version: Version.parse("6.30"), link: "https://public.simplyblk.xyz/6.30.rar", available: true),
FortniteBuild(version: Version.parse("6.31"), link: "https://public.simplyblk.xyz/6.31.rar", available: true),
FortniteBuild(version: Version.parse("7.00"), link: "https://public.simplyblk.xyz/7.00.rar", available: true),
FortniteBuild(version: Version.parse("7.10"), link: "https://public.simplyblk.xyz/7.10.rar", available: true),
FortniteBuild(version: Version.parse("7.20"), link: "https://public.simplyblk.xyz/7.20.rar", available: true),
FortniteBuild(version: Version.parse("7.30"), link: "https://public.simplyblk.xyz/7.30.zip", available: true),
FortniteBuild(version: Version.parse("7.40"), link: "https://public.simplyblk.xyz/7.40.rar", available: true),
FortniteBuild(version: Version.parse("8.00"), link: "https://public.simplyblk.xyz/8.00.zip", available: true),
FortniteBuild(version: Version.parse("8.20"), link: "https://public.simplyblk.xyz/8.20.rar", available: true),
FortniteBuild(version: Version.parse("8.30"), link: "https://public.simplyblk.xyz/8.30.rar", available: true),
FortniteBuild(version: Version.parse("8.40"), link: "https://public.simplyblk.xyz/8.40.zip", available: true),
FortniteBuild(version: Version.parse("8.50"), link: "https://public.simplyblk.xyz/8.50.zip", available: true),
FortniteBuild(version: Version.parse("8.51"), link: "https://public.simplyblk.xyz/8.51.rar", available: true),
FortniteBuild(version: Version.parse("9.00"), link: "https://public.simplyblk.xyz/9.00.zip", available: true),
FortniteBuild(version: Version.parse("9.01"), link: "https://public.simplyblk.xyz/9.01.zip", available: true),
FortniteBuild(version: Version.parse("9.10"), link: "https://public.simplyblk.xyz/9.10.rar", available: true),
FortniteBuild(version: Version.parse("9.21"), link: "https://public.simplyblk.xyz/9.21.zip", available: true),
FortniteBuild(version: Version.parse("9.30"), link: "https://public.simplyblk.xyz/9.30.zip", available: true),
FortniteBuild(version: Version.parse("9.40"), link: "https://public.simplyblk.xyz/9.40.zip", available: true),
FortniteBuild(version: Version.parse("9.41"), link: "https://public.simplyblk.xyz/9.41.rar", available: true),
FortniteBuild(version: Version.parse("10.00"), link: "https://public.simplyblk.xyz/10.00.zip", available: true),
FortniteBuild(version: Version.parse("10.10"), link: "https://public.simplyblk.xyz/10.10.zip", available: true),
FortniteBuild(version: Version.parse("10.20"), link: "https://public.simplyblk.xyz/10.20.zip", available: true),
FortniteBuild(version: Version.parse("10.31"), link: "https://public.simplyblk.xyz/10.31.zip", available: true),
FortniteBuild(version: Version.parse("10.40"), link: "https://public.simplyblk.xyz/10.40.rar", available: true),
FortniteBuild(version: Version.parse("11.00"), link: "https://public.simplyblk.xyz/11.00.zip", available: true),
FortniteBuild(version: Version.parse("11.31"), link: "https://public.simplyblk.xyz/11.31.rar", available: true),
FortniteBuild(version: Version.parse("12.00"), link: "https://public.simplyblk.xyz/12.00.rar", available: true),
FortniteBuild(version: Version.parse("12.21"), link: "https://public.simplyblk.xyz/12.21.zip", available: true),
FortniteBuild(version: Version.parse("12.50"), link: "https://public.simplyblk.xyz/12.50.zip", available: true),
FortniteBuild(version: Version.parse("12.61"), link: "https://public.simplyblk.xyz/12.61.zip", available: true),
FortniteBuild(version: Version.parse("13.00"), link: "https://public.simplyblk.xyz/13.00.rar", available: true),
FortniteBuild(version: Version.parse("13.40"), link: "https://public.simplyblk.xyz/13.40.zip", available: true),
FortniteBuild(version: Version.parse("14.00"), link: "https://public.simplyblk.xyz/14.00.rar", available: true),
FortniteBuild(version: Version.parse("14.40"), link: "https://public.simplyblk.xyz/14.40.rar", available: true),
FortniteBuild(version: Version.parse("14.60"), link: "https://public.simplyblk.xyz/14.60.rar", available: true),
FortniteBuild(version: Version.parse("15.30"), link: "https://public.simplyblk.xyz/15.30.rar", available: true),
FortniteBuild(version: Version.parse("16.40"), link: "https://public.simplyblk.xyz/16.40.rar", available: true),
FortniteBuild(version: Version.parse("17.30"), link: "https://public.simplyblk.xyz/17.30.zip", available: true),
FortniteBuild(version: Version.parse("17.50"), link: "https://public.simplyblk.xyz/17.50.zip", available: true),
FortniteBuild(version: Version.parse("18.40"), link: "https://public.simplyblk.xyz/18.40.zip", available: true),
FortniteBuild(version: Version.parse("19.10"), link: "https://public.simplyblk.xyz/19.10.rar", available: true),
FortniteBuild(version: Version.parse("20.40"), link: "https://public.simplyblk.xyz/20.40.zip", available: true),
];
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
try {
final stopped = _setupLifecycle(options);
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);
}
await outputFile.parent.create(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);
}
final downloadItemCompleter = Completer<File>();
delete(outputDir);
}catch(error) {
_onError(error, options);
}
}
await _startAriaServer();
final downloadId = await _startAriaDownload(options, outputFile);
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.tellStatus",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
}
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");
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)");
}
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;
}
timer.cancel();
return;
}
if(statusCode == 403 || statusCode == 503) {
throw _deniedConnectionError;
}
final speed = int.parse(result["downloadSpeed"] ?? "0");
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
final totalLength = int.parse(files[0]["length"] ?? "0");
if(statusCode == 404) {
throw _unavailableError;
}
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})";
}
});
throw _genericError;
},
headers: byteStart == null || byteStart <= 0 ? {
"Cookie": "_c_t_c=1"
} : {
"Cookie": "_c_t_c=1",
"Range": "bytes=${byteStart}-"
},
)
);
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) {
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);
_onError(error, options);
}finally {
delete(outputFile);
}
}
Future<void> _startAriaServer() async {
await stopDownloadServer();
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe";
}
final process = await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-secret=$_ariaSecret",
"--rpc-listen-port=$_ariaPort",
"--file-allocation=none",
"--check-certificate=false"
],
window: false
);
process.stdOutput.listen((message) => log("[ARIA] Message: $message"));
process.stdError.listen((error) => log("[ARIA] Error: $error"));
process.exitCode.then((exitCode) => log("[ARIA] Exit code: $exitCode"));
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) {
return;
}
await Future.delayed(const Duration(seconds: 1));
}
throw "cannot start download server (timeout exceeded)";
}
Future<bool> _isAriaRunning() async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.getVersion",
"params": [
"token:${_ariaSecret}"
]
};
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return response.statusCode == 200;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
"token:${_ariaSecret}",
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> stopDownloadServer() async {
await killProcessByPort(_ariaPort);
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
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";
throw "Missing 7zip.exe";
}
process = await startProcess(
@@ -176,10 +322,15 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
);
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);
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
@@ -190,7 +341,13 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
}
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(startTime, now, percentage, true, options);
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
@@ -206,7 +363,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
throw "Corrupted installation: missing winrar.exe";
throw "Missing winrar.exe";
}
process = await startProcess(
@@ -221,11 +378,16 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
);
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);
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
@@ -236,7 +398,13 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
}
final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options);
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
@@ -257,21 +425,22 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
process.kill(ProcessSignal.sigabrt);
}
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
if(percentage == 0) {
options.port.send(FortniteBuildDownloadProgress(
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting
extracting: extracting,
timeLeft: null,
speed: speed
));
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(
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
minutesLeft: minutesLeft
timeLeft: minutesLeft,
speed: speed
));
}
@@ -291,4 +460,5 @@ Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}
}

View File

@@ -5,19 +5,42 @@ 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";
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
const String kRebootBelowS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
const String kRebootAboveS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
const String _kRebootBelowS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootFallback.zip";
const String _kRebootAboveS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootS20Fallback.zip";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootDllFile.exists();
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
Future<void> downloadCriticalDll(String name, String outputPath) async {
Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
String? name;
switch(dll) {
case InjectableDll.console:
name = "console.dll";
case InjectableDll.auth:
name = "cobalt.dll";
case InjectableDll.memoryLeak:
name = "memory.dll";
case InjectableDll.gameServer:
name = null;
}
if(name == null) {
return;
}
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
@@ -28,13 +51,15 @@ Future<void> downloadCriticalDll(String name, String outputPath) async {
await output.writeAsBytes(response.bodyBytes, flush: true);
}
Future<int> downloadRebootDll(String url) async {
Future<void> downloadRebootDll(File file, String url, bool aboveS20) async {
Directory? outputDir;
final now = DateTime.now();
try {
final response = await http.get(Uri.parse(url));
var response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
throw Exception("Cannot download reboot.zip: status code ${response.statusCode}");
response = await http.get(Uri.parse(aboveS20 ? _kRebootAboveS20FallbackDownloadUrl : _kRebootBelowS20FallbackDownloadUrl));
if(response.statusCode != 200) {
throw Exception("status code ${response.statusCode}");
}
}
outputDir = await installationDirectory.createTemp("reboot_out");
@@ -42,8 +67,7 @@ Future<int> downloadRebootDll(String url) async {
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;
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
} finally{
if(outputDir != null) {
delete(outputDir);
@@ -55,17 +79,4 @@ Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}
Stream<String> watchDlls() async* {
if(_watcher) {
return;
}
_watcher = true;
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (event.path.endsWith(".dll")) {
yield event.path;
}
}
}
}

View File

@@ -7,7 +7,7 @@ final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1);
File _createLoggingFile() {
final file = File("${logsDirectory.path}\\launcher.log");
final file = File("${installationDirectory.path}\\launcher.log");
file.parent.createSync(recursive: true);
if(file.existsSync()) {
file.deleteSync();

View File

@@ -14,9 +14,6 @@ Directory get assetsDirectory {
return installationDirectory;
}
Directory get logsDirectory =>
Directory("${installationDirectory.path}\\logs");
Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings");

View File

@@ -1,15 +1,14 @@
// ignore_for_file: non_constant_identifier_names
import 'dart:async';
import 'dart:collection';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:ffi/ffi.dart';
import 'package: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');
@@ -96,62 +95,53 @@ Future<bool> startElevatedProcess({required String executable, required String a
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.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>();
var shellResult = ShellExecuteEx(shellInput);
return shellResult == 1;
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 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,
workingDirectory: workingDirectory,
environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _withLogger(name, executable, process, window);
return _ExtendedProcess(process, true);
}
final process = await Process.start(
executable.path,
args ?? [],
workingDirectory: executable.parent.path,
workingDirectory: workingDirectory,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _withLogger(name, executable, process, window);
return _ExtendedProcess(process, true);
}
_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();
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 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),
@@ -161,89 +151,78 @@ 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);
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
WaitForSingleObject(processHandle, INFINITE);
}finally {
return _NtSuspendProcess(processHandle) == 0;
} 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;
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtResumeProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
// TODO: Template
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) {
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
log("[PROCESS] Generating reboot args");
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
final args = [
"-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"
];
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(log) {
args.add("-log");
if(logging) {
args["-log"] = "";
}
if(host) {
args.addAll([
"-nosplash",
"-nosound"
]);
args["-nosplash"] = "";
args["-nosound"] = "";
if(hostType == GameServerType.headless){
args.add("-nullrhi");
args["-nullrhi"] = "";
}
}
if(additionalArgs.isNotEmpty){
args.addAll(additionalArgs.split(" "));
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");
}
return args;
log("[PROCESS] Final args result: $args");
return args.entries
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
.toList();
}
void handleGameOutput({
@@ -257,25 +236,27 @@ void handleGameOutput({
required void Function() onBuildCorrupted,
}) {
if (line.contains(kShutdownLine)) {
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd();
}else if(line.contains(kDisplayInitializedLine) && host) {
}else if(line.contains(kDisplayLine) && line.contains(kDisplayInitializedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
onDisplayAttached();
}
}
String _parseUsername(String username, bool host) {
if(host) {
return "Player${Random().nextInt(1000)}";
}
if (username.isEmpty) {
return kDefaultPlayerName;
}
@@ -297,7 +278,6 @@ final class _ExtendedProcess implements Process {
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
@override
Future<int> get exitCode => _delegate.exitCode;

View File

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

View File

@@ -1,16 +1,39 @@
# reboot_launcher
Launcher for project reboot
# Reboot Launcher
Welcome to the **Reboot Launcher**!
This is a GUI application developed as part of the **Reboot Project**.
## Getting Started
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)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
### Packaging the Project
To package the application for distribution, run:
```
package.bat
```
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## Requirements
- [Flutter SDK](https://flutter.dev/docs/get-started/install)
- 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.

Binary file not shown.

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

Binary file not shown.

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -76,10 +76,10 @@
"playGameServerCustomContent": "Enter IP",
"settingsName": "Settings",
"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",
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
"settingsClientConsoleName": "Unreal engine console",
"settingsClientConsoleName": "Unreal engine patcher",
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
"settingsClientConsoleKeyName": "Unreal engine console key",
"settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console",
@@ -88,24 +88,25 @@
"settingsClientMemoryName": "Memory patcher",
"settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak",
"settingsClientArgsName": "Custom launch arguments",
"settingsClientArgsDescription": "Additional arguments to use when launching the game",
"settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
"settingsClientArgsPlaceholder": "Arguments...",
"settingsServerName": "Internal files",
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
"settingsServerOptionsName": "Options",
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
"settingsServerTypeName": "Type",
"settingsServerTypeName": "Game server type",
"settingsServerTypeDescription": "The type of game server to inject",
"settingsServerTypeEmbeddedName": "Embedded",
"settingsServerTypeCustomName": "Custom",
"settingsServerFileName": "Implementation",
"settingsOldServerFileName": "Game server",
"settingsServerFileDescription": "The file injected to create the game server",
"settingsServerPortName": "Port",
"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",
"settingsServerMirrorPlaceholder": "mirror",
"settingsServerTimerName": "Update timer",
"settingsServerTimerName": "Game server updater",
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
"settingsUtilsName": "Launcher",
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
@@ -118,9 +119,7 @@
"settingsUtilsResetDefaultsName": "Reset settings",
"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",
"settingsUtilsResetDefaultsContent": "Reset",
"settingsUtilsDialogSecondaryAction": "Close",
"settingsUtilsDialogPrimaryAction": "Reset",
"selectFortniteName": "Fortnite version",
"selectFortniteDescription": "Select the version of Fortnite you want to use",
"manageVersionsName": "Manage versions",
@@ -129,7 +128,7 @@
"importVersionDescription": "Import a new version of Fortnite into the launcher",
"addLocalBuildName": "Add a version from this PC's local storage",
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
"addVersion": "Add version",
"addVersion": "New version",
"downloadBuildName": "Download any version from the cloud",
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
"downloadBuildContent": "Download build",
@@ -147,6 +146,7 @@
"defaultServerName": "Reboot Game Server",
"defaultServerDescription": "Just another server",
"downloadingDll": "Downloading {name} dll...",
"dllAlreadyExists": "The {name} was already downloaded",
"downloadDllSuccess": "The {name} dll was downloaded successfully",
"downloadDllError": "An error occurred while downloading {name}: {error}",
"downloadDllRetry": "Retry",
@@ -156,6 +156,7 @@
"launchingGameClientAndServer": "Launching the game client and server...",
"startGameServer": "Start a game server",
"usernameOrEmail": "Username/Email",
"invalidEmail": "Invalid email",
"usernameOrEmailPlaceholder": "Type your username or email",
"password": "Password",
"passwordPlaceholder": "Type your password, if you want to use one",
@@ -215,6 +216,7 @@
"downloadedVersion": "The download was completed successfully!",
"download": "Download",
"downloading": "Downloading...",
"startingDownload": "Starting download...",
"extracting": "Extracting...",
"buildProgress": "{progress}%",
"buildInstallationDirectory": "Installation directory",
@@ -234,7 +236,7 @@
"startGame": "Start fortnite",
"stopGame": "Close fortnite",
"waitingForGameServer": "Waiting for the game server to boot up...",
"gameServerStartWarning": "The game server was started successfully, but Reboot didn't load",
"gameServerStartWarning": "Unsupported version: the game server crashed while setting up the server",
"gameServerStartLocalWarning": "The game server was started successfully, but other players can't join",
"gameServerStarted": "The game server was started successfully",
"gameClientStarted": "The game client was started successfully",
@@ -261,6 +263,7 @@
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
"fortniteCrashError": "The {name} crashed after being launched",
"serverNoLongerAvailableUnnamed": "The previous server is no longer available",
"noServerFound": "No server found: invalid or expired link",
"settingsUtilsThemeName": "Theme",
@@ -320,8 +323,11 @@
"none": "none",
"openLog": "Open log",
"backendProcessError": "The backend shut down unexpectedly",
"backendErrorMessage": "The backend reported an unexpected error",
"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",
"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",
"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",
@@ -359,9 +365,14 @@
"promptBackendDetachedActionLabel": "Next",
"promptInfoTabText": "The Info tab contains useful links to report bugs and receive support",
"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",
"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",
"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"
}

View File

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

View File

@@ -1,49 +1,61 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
class BackendController extends GetxController {
late final GetStorage? storage;
static const String storageName = "v2_backend_storage";
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage? _storage;
late final TextEditingController host;
late final TextEditingController port;
late final Rx<ServerType> type;
late final TextEditingController gameServerAddress;
late final FocusNode gameServerAddressFocusNode;
late final Rx<PhysicalKeyboardKey> consoleKey;
late final RxBool started;
late final RxBool detached;
StreamSubscription? worker;
int? embeddedProcessPid;
HttpServer? localServer;
HttpServer? remoteServer;
late final List<InfoBarEntry> _infoBars;
StreamSubscription? _worker;
ServerImplementation? _implementation;
BackendController() {
storage = appWithNoStorage ? null : GetStorage("backend_storage");
_storage = appWithNoStorage ? null : GetStorage(storageName);
started = RxBool(false);
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0));
type.listen((value) {
host.text = _readHost();
port.text = _readPort();
storage?.write("type", value.index);
if (!started.value) {
return;
}
stop();
_storage?.write("type", value.index);
});
host = TextEditingController(text: _readHost());
host.addListener(() =>
storage?.write("${type.value.name}_host", host.text));
_storage?.write("${type.value.name}_host", host.text));
port = TextEditingController(text: _readPort());
port.addListener(() =>
storage?.write("${type.value.name}_port", port.text));
detached = RxBool(storage?.read("detached") ?? false);
detached.listen((value) => storage?.write("detached", value));
final address = storage?.read("game_server_address");
_storage?.write("${type.value.name}_port", port.text));
detached = RxBool(_storage?.read("detached") ?? false);
detached.listen((value) => _storage?.write("detached", value));
final address = _storage?.read("game_server_address");
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue);
@@ -55,7 +67,7 @@ class BackendController extends GetxController {
lastValue = newValue;
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
storage?.write("game_server_address", newValue);
_storage?.write("game_server_address", newValue);
writeMatchmakingIp(newValue);
});
watchMatchmakingIp().listen((event) {
@@ -64,27 +76,40 @@ class BackendController extends GetxController {
}
});
gameServerAddressFocusNode = FocusNode();
consoleKey = Rx(() {
final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) {
return _kDefaultConsoleKey;
}
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
if(consoleKeyNumber == null) {
return _kDefaultConsoleKey;
}
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
if(!consoleKey.isUnrealEngineKey) {
return _kDefaultConsoleKey;
}
return consoleKey;
}());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
_infoBars = [];
}
void joinLocalhost() {
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";
detached.value = false;
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? value = storage?.read("${type.value.name}_host");
String? value = _storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) {
return value;
}
@@ -96,152 +121,427 @@ class BackendController extends GetxController {
return "";
}
String _readPort() =>
storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
String _readPort() => _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
Stream<ServerResult> start() async* {
try {
if(started.value) {
return;
}
void joinLocalhost() {
gameServerAddress.text = kDefaultGameServerHost;
}
final hostData = this.host.text.trim();
final portData = this.port.text.trim();
if(type() != ServerType.local) {
started.value = true;
yield ServerResult(ServerResultType.starting);
}else {
started.value = false;
if(portData != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting);
}
}
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);
}
if (hostData.isEmpty) {
yield ServerResult(ServerResultType.missingHostError);
started.value = false;
return;
}
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
port.text = kDefaultBackendPort.toString();
gameServerAddress.text = "127.0.0.1";
consoleKey.value = _kDefaultConsoleKey;
detached.value = false;
}
if (portData.isEmpty) {
yield ServerResult(ServerResultType.missingPortError);
started.value = false;
return;
}
final portNumber = int.tryParse(portData);
if (portNumber == null) {
yield ServerResult(ServerResultType.illegalPortError);
started.value = false;
return;
}
if ((type() != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
yield ServerResult(ServerResultType.freeingPort);
final result = await freeBackendPort();
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
if(!result) {
started.value = false;
return;
}
}
switch(type()){
case ServerType.embedded:
final process = await startEmbeddedBackend(detached.value);
embeddedProcessPid = process.pid;
break;
case ServerType.remote:
yield ServerResult(ServerResultType.pingingRemote);
final uriResult = await pingBackend(hostData, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
started.value = false;
return;
}
remoteServer = await startRemoteBackendProxy(uriResult);
break;
case ServerType.local:
if(portNumber != kDefaultBackendPort) {
yield ServerResult(ServerResultType.pingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
started.value = false;
return;
}
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
}
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;
Future<bool> toggle() {
if(started.value) {
return stop(interactive: true);
}else {
return start(interactive: true);
}
}
Stream<ServerResult> stop() async* {
Future<bool> start({required bool interactive}) async {
if(started.value) {
return true;
}
_cancel();
final stream = startBackend(
type: type.value,
host: host.text,
port: port.text,
detached: detached.value,
onError: (errorMessage) {
stop(interactive: false);
_showRebootInfoBar(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
}
);
final completer = Completer<bool>();
InfoBarEntry? entry;
_worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event, interactive);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
completer.complete(true);
}
});
return await completer.future;
}
Future<bool> stop({required bool interactive}) async {
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;
}
yield ServerResult(ServerResultType.stopping);
started.value = false;
try{
switch(type()){
case ServerType.embedded:
final embeddedProcessPid = this.embeddedProcessPid;
if(embeddedProcessPid != null) {
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm);
this.embeddedProcessPid = null;
}
break;
case ServerType.remote:
await remoteServer?.close(force: true);
remoteServer = null;
break;
case ServerType.local:
await localServer?.close(force: true);
localServer = null;
break;
}
yield ServerResult(ServerResultType.stopSuccess);
}catch(error, stackTrace){
yield ServerResult(
ServerResultType.stopError,
error: error,
stackTrace: stackTrace
final version = Get.find<GameController>()
.getVersionByName(server.version.toString());
if(version == null) {
_showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
started.value = true;
return;
}
final hashedPassword = server.password;
final hasPassword = hashedPassword != null;
final embedded = type.value == ServerType.embedded;
final author = server.author;
final encryptedIp = server.ip;
if(!hasPassword) {
final valid = await _isServerValid(encryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, encryptedIp, author, version);
return;
}
final confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
_showRebootInfoBar(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
final valid = await _isServerValid(decryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, decryptedIp, author, version);
}
Stream<ServerResult> toggle() async* {
if(started()) {
yield* stop();
Future<bool> _isServerValid(String address) async {
final result = await pingGameServer(address);
if(result) {
return true;
}
_showRebootInfoBar(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.serverPassword,
child: Obx(() => TextFormBox(
placeholder: translations.serverPasswordPlaceholder,
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
),
)
))
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: translations.serverPasswordCancel,
type: ButtonType.secondary
),
DialogButton(
text: translations.serverPasswordConfirm,
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
)
);
}
void _onServerJoined(bool embedded, String decryptedIp, String author, FortniteVersion version) {
if(embedded) {
gameServerAddress.text = decryptedIp;
pageIndex.value = RebootPageType.play.index;
}else {
yield* start();
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>()
.selectedVersion = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,
severity: InfoBarSeverity.success
));
}
InfoBarEntry _showRebootInfoBar(dynamic text, {
InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false,
Duration? duration = infoBarShortDuration,
void Function()? onDismissed,
Widget? action
}) {
final result = showRebootInfoBar(
text,
severity: severity,
loading: loading,
duration: duration,
onDismissed: onDismissed,
action: action
);
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
_infoBars.add(result);
}
return result;
}
Future<void> restart() async {
if(started.value) {
await stop(interactive: false);
await start(interactive: true);
}
}
}

View File

@@ -0,0 +1,289 @@
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:version/version.dart';
import 'package:path/path.dart' as path;
class DllController extends GetxController {
static const String storageName = "v2_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;
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("update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text));
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));
}
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;
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
);
}
await Future.wait(
[
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text, false),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text, true),
Future.delayed(const Duration(seconds: 1))
],
eagerError: false
);
timestamp.value = DateTime.now().millisecondsSinceEpoch;
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;
infoBarEntry = showRebootInfoBar(
translations.downloadDllError(error.toString(), "reboot.dll"),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () async {
infoBarEntry?.close();
updateGameServerDll(
force: true,
silent: silent
);
},
child: Text(translations.downloadDllRetry),
)
);
return false;
}
}
(File, bool) getInjectableData(Version version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.gameServer:
if(customGameServer.value) {
return (File(customGameServerDll.text), true);
}
return (version.major >= 20 ? 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);
}
}
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] File already exists");
return true;
}
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadDependency(dll, 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(error.toString(), dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await download(dll, filePath, silent: silent, force: force);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
}
}
void guardFiles() {
for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
if (path.equals(controller.text, defaultPath)) {
download(injectable, controller.text);
}
controller.addListener(() async {
try {
if (!path.equals(controller.text, defaultPath)) {
return;
}
final filePath = controller.text;
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (path.equals(event.path, filePath)) {
await download(injectable, filePath);
}
}
} catch(_) {
// Ignore
}
});
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
}

View File

@@ -1,18 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import '../../main.dart';
import 'package:reboot_launcher/main.dart';
class GameController extends GetxController {
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
static const String storageName = "v2_game_storage";
late final GetStorage? _storage;
late final TextEditingController username;
@@ -22,10 +18,9 @@ class GameController extends GetxController {
late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool started;
late final Rxn<GameInstance> instance;
late final Rx<PhysicalKeyboardKey> consoleKey;
GameController() {
_storage = appWithNoStorage ? null : GetStorage("game_storage");
_storage = appWithNoStorage ? null : GetStorage(storageName);
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
final decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry))
@@ -41,41 +36,9 @@ class GameController extends GetxController {
password = TextEditingController(text: _storage?.read("password") ?? "");
password.addListener(() => _storage?.write("password", password.text));
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() =>
_storage?.write("custom_launch_args", customLaunchArgs.text));
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false);
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() {
@@ -83,6 +46,7 @@ class GameController extends GetxController {
password.text = "";
customLaunchArgs.text = "";
versions.value = [];
_selectedVersion.value = null;
instance.value = null;
}

View File

@@ -12,8 +12,12 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart';
class HostingController extends GetxController {
static const String storageName = "v2_hosting_storage";
late final GetStorage? _storage;
late final String uuid;
late final TextEditingController accountUsername;
late final TextEditingController accountPassword;
late final TextEditingController name;
late final FocusNode nameFocusNode;
late final TextEditingController description;
@@ -28,12 +32,17 @@ class HostingController extends GetxController {
late final RxBool published;
late final Rxn<GameInstance> instance;
late final Rxn<Set<FortniteServer>> servers;
late final TextEditingController customLaunchArgs;
late final Semaphore _semaphore;
HostingController() {
_storage = appWithNoStorage ? null : GetStorage("hosting_storage");
_storage = appWithNoStorage ? null : GetStorage(storageName);
uuid = _storage?.read("uuid") ?? const Uuid().v4();
_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.addListener(() => _storage?.write("name", name.text));
description = TextEditingController(text: _storage?.read("description"));
@@ -53,16 +62,34 @@ class HostingController extends GetxController {
published = RxBool(false);
showPassword = RxBool(false);
instance = Rxn();
final supabase = Supabase.instance.client;
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")
.stream(primaryKey: ['id'])
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
.listen((event) {
servers.value = event;
published.value = event.any((element) => element.id == uuid);
});
_semaphore = Semaphore();
.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;
published.value = event.any((element) => element.id == uuid);
}
Future<void> publishServer(String author, String version) async {
@@ -131,14 +158,16 @@ class HostingController extends GetxController {
}
void reset() {
accountUsername.text = kDefaultHostName;
accountPassword.text = "";
name.text = "";
description.text = "";
showPassword.value = false;
discoverable.value = false;
started.value = false;
instance.value = null;
type.value = GameServerType.headless;
autoRestart.value = true;
customLaunchArgs.text = "";
}
FortniteServer? findServerById(String uuid) {

View File

@@ -1,51 +1,31 @@
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:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
class SettingsController extends GetxController {
static const String storageName = "v2_settings_storage";
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 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 Map<String, Future<bool>> _operations;
late double width;
late double height;
late double? offsetX;
late double? offsetY;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
SettingsController() {
_storage = appWithNoStorage ? null : GetStorage("settings_storage");
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));
_storage = appWithNoStorage ? null : GetStorage(storageName);
width = _storage?.read("width") ?? kDefaultWindowWidth;
height = _storage?.read("height") ?? kDefaultWindowHeight;
offsetX = _storage?.read("offset_x");
@@ -54,25 +34,8 @@ class SettingsController extends GetxController {
themeMode.listen((value) => _storage?.write("theme", value.index));
language = RxString(_storage?.read("language") ?? currentLocale);
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.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) {
@@ -87,32 +50,18 @@ class SettingsController extends GetxController {
_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 {
if(appVersion == null) {
if (appVersion == null) {
return;
}
final pubspec = await _getPubspecYaml();
if(pubspec == null) {
if (pubspec == null) {
return;
}
final latestVersion = Version.parse(pubspec["version"]);
if(latestVersion <= appVersion) {
if (latestVersion <= appVersion) {
return;
}
@@ -125,7 +74,8 @@ class SettingsController extends GetxController {
child: Text(translations.updateAvailableAction),
onPressed: () {
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 {
try {
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if(pubspecResponse.statusCode != 200) {
final pubspecResponse = await http.get(Uri.parse(
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if (pubspecResponse.statusCode != 200) {
return null;
}
return loadYaml(pubspecResponse.body);
}catch(error) {
} catch (error) {
log("[UPDATER] Cannot check for updates: $error");
return null;
}
}
Future<bool> updateReboot({bool force = false, bool silent = false}) async {
if(_updater != null) {
return await _updater!;
}
final result = _updateReboot(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateReboot(bool force, bool silent) async {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
return true;
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
infoBarEntry?.close();
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
showRebootInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => updateReboot(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry),
)
);
return false;
}finally {
_updater = null;
}
}
(File, bool) getInjectableData(InjectableDll dll) {
final defaultPath = canonicalize(_getDefaultPath(dll));
switch(dll){
case InjectableDll.reboot:
if(customGameServer.value) {
final file = File(gameServerDll.text);
if(file.existsSync()) {
return (file, true);
}
}
return (rebootDllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.cobalt:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memory:
final memoryLeakFile = File(memoryLeakDll.text);
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
}
}
String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
log("[DLL] Asking for $filePath(silent: $silent)");
final old = _operations[filePath];
if(old != null) {
log("[DLL] Download task already exists");
return old;
}
log("[DLL] Creating new download task...");
final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun;
return newRun;
}
Future<bool> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
InfoBarEntry? entry;
try {
if (fileName == "reboot.dll") {
log("[DLL] Downloading reboot.dll...");
return await updateReboot(
silent: silent
);
}
if(File(filePath).existsSync()) {
log("[DLL] File already exists");
return true;
}
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
entry?.close();
if(!silent) {
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
log("[DLL] Error: $message");
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
}finally {
_operations.remove(fileName);
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
}

View File

@@ -1,7 +1,7 @@
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
@@ -300,7 +300,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _primaryButton => Button(
style: ButtonStyle(
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
),
onPressed: widget.onTap!,
child: Text(widget.text!),
@@ -308,7 +308,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _secondaryButton => Button(
style: widget.color != null ? ButtonStyle(
backgroundColor: ButtonState.all(widget.color!)
backgroundColor: WidgetStateProperty.all(widget.color!)
) : null,
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),

View File

@@ -1,313 +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';
final List<InfoBarEntry> _infoBars = [];
extension ServerControllerDialog on BackendController {
void cancelInteractive() {
worker?.cancel(); // Do not await or it will hang
_infoBars.forEach((infoBar) => infoBar.close());
_infoBars.clear();
}
Future<bool> toggleInteractive() async {
cancelInteractive();
final stream = toggle();
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:
final embeddedProcessPid = this.embeddedProcessPid;
if(embeddedProcessPid != null) {
watchProcess(embeddedProcessPid).then((_) {
if(started.value) {
started.value = false;
_showRebootInfoBar(
translations.backendProcessError,
severity: InfoBarSeverity.error
);
}
});
}
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success
);
case ServerResultType.startError:
print(event.stackTrace);
return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.stopping:
return _showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.stopSuccess:
return _showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
case ServerResultType.stopError:
return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.missingHostError:
return _showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
case ServerResultType.missingPortError:
return _showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.illegalPortError:
return _showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.freeingPort:
return _showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
case ServerResultType.freePortSuccess:
return _showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
case ServerResultType.freePortError:
return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.pingingRemote:
return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingingLocal:
return _showRebootInfoBar(
translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingError:
return _showRebootInfoBar(
translations.pingError(type.value.name),
severity: InfoBarSeverity.error
);
}
}
Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
if(!kDebugMode && uuid == server.id) {
_showRebootInfoBar(
translations.joinSelfServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final gameController = Get.find<GameController>();
final version = gameController.getVersionByName(server.version.toString());
if(version == null) {
_showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final hashedPassword = server.password;
final hasPassword = hashedPassword != null;
final embedded = type.value == ServerType.embedded;
final author = server.author;
final encryptedIp = server.ip;
if(!hasPassword) {
final valid = await _isServerValid(encryptedIp);
if(!valid) {
return;
}
_onSuccess(gameController, embedded, encryptedIp, author, version);
return;
}
final confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
_showRebootInfoBar(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
final valid = await _isServerValid(decryptedIp);
if(!valid) {
return;
}
_onSuccess(gameController, embedded, decryptedIp, author, version);
}
Future<bool> _isServerValid(String address) async {
final result = await pingGameServer(address);
if(result) {
return true;
}
_showRebootInfoBar(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.serverPassword,
child: Obx(() => TextFormBox(
placeholder: translations.serverPasswordPlaceholder,
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: 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
));
}
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
);
_infoBars.add(result);
return result;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -1,44 +1,34 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:reboot_common/common.dart';
const Duration _timeout = Duration(seconds: 5);
Future<bool> pingGameServer(String address, {Duration? timeout}) async {
Future<bool> ping(String hostname, int port) async {
log("[MATCHMAKER] Pinging $hostname:$port");
RawDatagramSocket? socket;
try {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
await for (final event in socket) {
log("[MATCHMAKER] Event: $event");
switch(event) {
case RawSocketEvent.read:
log("[MATCHMAKER] Success");
return true;
case RawSocketEvent.write:
log("[MATCHMAKER] Sending data");
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
socket.send(dataToSend, InternetAddress(hostname), port);
case RawSocketEvent.readClosed:
case RawSocketEvent.closed:
return false;
}
}
Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
final completer = Completer<bool>();
final start = DateTime.now();
_pingGameServerOrTimeout(completer, start, timeout, address);
return completer;
}
return false;
}catch(error) {
log("[MATCHMAKER] Error: $error");
return false;
}finally {
socket?.close();
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);
}
}
final start = DateTime.now();
var firstTime = true;
Future<bool> pingGameServer(String address) async {
final split = address.split(":");
var hostname = split[0];
if(isLocalHost(hostname)) {
@@ -46,19 +36,37 @@ Future<bool> pingGameServer(String address, {Duration? timeout}) async {
}
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;
return await _ping(hostname, port)
.timeout(_timeout, onTimeout: () => false);
}
Future<bool> _ping(String hostname, int port) async {
log("[MATCHMAKER] Pinging $hostname:$port");
RawDatagramSocket? socket;
try {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
await for (final event in socket) {
log("[MATCHMAKER] Event: $event");
switch(event) {
case RawSocketEvent.read:
log("[MATCHMAKER] Success");
return true;
case RawSocketEvent.write:
log("[MATCHMAKER] Sending data");
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
socket.send(dataToSend, InternetAddress(hostname), port);
case RawSocketEvent.readClosed:
case RawSocketEvent.closed:
return false;
}
}
if(firstTime) {
firstTime = false;
}else {
await Future.delayed(_timeout);
}
return false;
}catch(error) {
log("[MATCHMAKER] Error: $error");
return false;
}finally {
socket?.close();
}
return false;
}

View File

@@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/scheduler.dart';
import 'package:win32/win32.dart';
import 'package:window_manager/window_manager.dart';
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -24,10 +25,13 @@ bool get isWin11 {
return intBuild != null && intBuild > 22000;
}
Future<String?> openFolderPicker(String title) async =>
await FilePicker.platform.getDirectoryPath(dialogTitle: title);
Future<String?> openFolderPicker(String title) async {
FilePicker.platform = FilePickerWindows();
return await FilePicker.platform.getDirectoryPath(dialogTitle: title);
}
Future<String?> openFilePicker(String extension) async {
FilePicker.platform = FilePickerWindows();
var result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowMultiple: false,
@@ -93,7 +97,7 @@ class IVirtualDesktop extends IUnknown {
throw WindowsException(code);
}
return convertFromHString(result.value);
return _convertFromHString(result.value);
}
}
@@ -280,7 +284,7 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
HRESULT Function(Pointer, COMObject, Int8)>>>()
.value
.asFunction<int Function(Pointer, COMObject, int)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName));
ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
if (code != 0) {
throw WindowsException(code);
}
@@ -369,7 +373,7 @@ List<int> _getHWnds(int pid, String? excludedWindowName) {
result.ref.excluded = excludedWindowName.toNativeUtf16();
}
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
EnumWindows(Pointer.fromFunction<WNDENUMPROC>(_filter, TRUE), result.address);
final length = result.ref.HWndLength;
final HWndsPointer = result.ref.HWnd;
if(HWndsPointer == nullptr) {
@@ -397,7 +401,7 @@ class VirtualDesktopManager {
}
final hr = CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
if (FAILED(hr)) {
throw WindowsException(hr);
}
@@ -468,3 +472,77 @@ class VirtualDesktopManager {
void setDesktopName(IVirtualDesktop desktop, String newName) =>
windowManager.setDesktopName(desktop, newName);
}
String _convertFromHString(int hstring) =>
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
int _convertToHString(String string) {
final hString = calloc<HSTRING>();
final stringPtr = string.toNativeUtf16();
try {
final hr = WindowsCreateString(stringPtr, string.length, hString);
if (FAILED(hr)) throw WindowsException(hr);
return hString.value;
} finally {
free(stringPtr);
free(hString);
}
}
extension WindowManagerExtension on WindowManager {
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
}
class WindowsDisk {
static final String _nullTerminator = String.fromCharCode(0);
final String path;
final int freeBytesAvailable;
final int totalNumberOfBytes;
const WindowsDisk._internal(this.path, this.freeBytesAvailable, this.totalNumberOfBytes);
static List<WindowsDisk> available() {
final buffer = malloc.allocate<Utf16>(MAX_PATH);
try {
final length = GetLogicalDriveStrings(MAX_PATH, buffer);
if (length == 0) {
return [];
}
return buffer.toDartString(length: length)
.split(_nullTerminator)
.where((drive) => drive.length > 1)
.map((driveName) {
final freeBytesAvailable = calloc<Uint64>();
final totalNumberOfBytes = calloc<Uint64>();
final totalNumberOfFreeBytes = calloc<Uint64>();
try {
GetDiskFreeSpaceEx(
driveName.toNativeUtf16(),
freeBytesAvailable,
totalNumberOfBytes,
totalNumberOfFreeBytes
);
return WindowsDisk._internal(
driveName,
freeBytesAvailable.value,
totalNumberOfBytes.value
);
} finally {
calloc.free(freeBytesAvailable);
calloc.free(totalNumberOfBytes);
calloc.free(totalNumberOfFreeBytes);
}
})
.toList(growable: false);
} finally {
calloc.free(buffer);
}
}
@override
String toString() {
return 'WindowsDisk{path: $path, freeBytesAvailable: $freeBytesAvailable, totalNumberOfBytes: $totalNumberOfBytes}';
}
}

View File

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

View File

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

View File

@@ -19,10 +19,10 @@ class FileSelector extends StatefulWidget {
required this.controller,
required this.validator,
required this.folder,
required this.allowNavigator,
this.label,
this.extension,
this.validatorMode,
this.allowNavigator = true,
Key? key})
: assert(folder || extension != null, "Missing extension for file selector"),
super(key: key);

View File

@@ -0,0 +1,121 @@
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;
// FIXME: If the user clicks on the reset button, the text field checker won't be called
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
final obx = RxString(controller.text);
controller.addListener(() => obx.value = controller.text);
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: _checkDll,
extension: "dll",
folder: false,
validatorMode: AutovalidateMode.always,
allowNavigator: false,
),
),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(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: _checkDll(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);
if (!file.existsSync()) {
return translations.dllDoesNotExist;
}
if (!text.endsWith(".dll")) {
return translations.invalidDllExtension;
}
return null;
}

View File

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

View File

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

View File

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

View File

@@ -9,18 +9,18 @@ import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart';
class LaunchButton extends StatefulWidget {
final bool host;
@@ -39,11 +39,12 @@ class _LaunchButtonState extends State<LaunchButton> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation;
Completer? _pingOperation;
IVirtualDesktop? _virtualDesktop;
@override
@@ -92,11 +93,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(injectable, host) == null) {
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
);
if(await _getDllFileOrStop(version.content, injectable, host) == null) {
return;
}
}
@@ -113,7 +110,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
final backendResult = _backendController.started() || await _backendController.toggleInteractive();
final backendResult = _backendController.started() || await _backendController.toggle();
if(!backendResult){
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop(
@@ -122,7 +119,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
log("[${host ? 'HOST' : 'GAME'}] Backend works");
final serverType = _hostingController.type.value;
final serverType = _hostingController.type.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
@@ -138,6 +135,12 @@ class _LaunchButtonState extends State<LaunchButton> {
}else {
_showLaunchingGameServerWidget();
}
} on ProcessException catch (exception, stackTrace) {
_onStop(
reason: _StopReason.corruptedVersionError,
error: exception.toString(),
stackTrace: stackTrace
);
} catch (exception, stackTrace) {
_onStop(
reason: _StopReason.unknownError,
@@ -225,7 +228,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance(
versionName: version.content.toString(),
version: version.content,
gamePid: gameProcess,
launcherPid: launcherProcess,
eacPid: eacProcess,
@@ -238,7 +241,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{
_gameController.instance.value = instance;
}
await _injectOrShowError(InjectableDll.cobalt, host);
await _injectOrShowError(InjectableDll.auth, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance;
}
@@ -246,12 +249,12 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs(
_gameController.username.text,
_gameController.password.text,
host ? _hostingController.accountUsername.text : _gameController.username.text,
host ? _hostingController.accountPassword.text : _gameController.password.text,
host,
hostType,
false,
""
host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
);
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
final gameProcess = await startProcess(
@@ -263,15 +266,23 @@ class _LaunchButtonState extends State<LaunchButton> {
"OPENSSL_ia32cap": "~0x20000000"
}
);
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
void onGameOutput(String line, bool error) {
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
handleGameOutput(
line: line,
host: host,
onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError),
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError),
onBuildCorrupted: () {
if(instance == null) {
return;
}else if(!instance.launched) {
_onStop(reason: _StopReason.corruptedVersionError);
}else {
_onStop(reason: _StopReason.crash);
}
},
onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version),
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
@@ -386,16 +397,16 @@ class _LaunchButtonState extends State<LaunchButton> {
if(instance != null && !instance.launched) {
instance.launched = true;
instance.tokenError = false;
await _injectOrShowError(InjectableDll.memory, host);
await _injectOrShowError(InjectableDll.memoryLeak, host);
if(!host){
await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected();
}else {
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text);
final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
if(gameServerPort != null) {
await killProcessByPort(gameServerPort);
}
await _injectOrShowError(InjectableDll.reboot, host);
await _injectOrShowError(InjectableDll.gameServer, host);
_onGameServerInjected();
}
}
@@ -424,11 +435,13 @@ class _LaunchButtonState extends State<LaunchButton> {
loading: true,
duration: null
);
final gameServerPort = _settingsController.gameServerPort.text;
final localPingResult = await pingGameServer(
final gameServerPort = _dllController.gameServerPort.text;
final pingOperation = pingGameServerOrTimeout(
"127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 2)
const Duration(minutes: 2)
);
this._pingOperation = pingOperation;
final localPingResult = await pingOperation.future;
_gameServerInfoBar?.close();
if (!localPingResult) {
showRebootInfoBar(
@@ -450,8 +463,8 @@ class _LaunchButtonState extends State<LaunchButton> {
}
await _hostingController.publishServer(
_gameController.username.text,
_hostingController.instance.value!.versionName,
_hostingController.accountUsername.text,
_hostingController.instance.value!.version.toString(),
);
showRebootInfoBar(
translations.gameServerStarted,
@@ -471,16 +484,18 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
final publicIp = await Ipify.ipv4();
final externalResult = await pingGameServer("$publicIp:$gameServerPort");
if (externalResult) {
final available = await pingGameServer("$publicIp:$gameServerPort");
if(available) {
_gameServerInfoBar?.close();
return true;
}
_gameServerInfoBar?.close();
final future = pingGameServer(
final pingOperation = pingGameServerOrTimeout(
"$publicIp:$gameServerPort",
timeout: const Duration(days: 365)
const Duration(days: 1)
);
this._pingOperation = pingOperation;
_gameServerInfoBar?.close();
_gameServerInfoBar = showRebootInfoBar(
translations.checkGameServerFixMessage(gameServerPort),
action: Button(
@@ -491,21 +506,32 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null,
loading: true
);
return await future;
}finally {
final result = await pingOperation.future;
_gameServerInfoBar?.close();
return result;
}catch(_) {
_gameServerInfoBar?.close();
return false;
}
}
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
if(host == null) {
try {
_pingOperation?.complete(false);
}catch(_) {
// Ignore: might be running, don't bother checking
} finally {
_pingOperation = null;
}
await _operation?.cancel();
_operation = null;
_backendController.cancelInteractive();
_backendController.stop(interactive: false);
}
host = host ?? widget.host;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){
_hostingController.instance.value = null;
}else {
@@ -527,19 +553,17 @@ class _LaunchButtonState extends State<LaunchButton> {
_hostingController.discardServer();
}
if(instance != null) {
if(reason == _StopReason.normal) {
instance.launched = true;
}
if(reason == _StopReason.normal) {
instance?.launched = true;
}
instance.kill();
final child = instance.child;
if(child != null) {
await _onStop(
reason: reason,
host: child.serverType != null
);
}
instance?.kill();
final child = instance?.child;
if(child != null) {
await _onStop(
reason: reason,
host: child.serverType != null
);
}
_setStarted(host, false);
@@ -581,9 +605,9 @@ class _LaunchButtonState extends State<LaunchButton> {
break;
case _StopReason.corruptedVersionError:
showRebootInfoBar(
translations.corruptedVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
translations.corruptedVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
@@ -605,14 +629,22 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.tokenError:
_backendController.stop(interactive: false);
showRebootInfoBar(
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none),
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? "game server" : "client"),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
break;
case _StopReason.unknownError:
@@ -635,7 +667,7 @@ class _LaunchButtonState extends State<LaunchButton> {
try {
final gameProcess = instance.gamePid;
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");
if(dllPath == null) {
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
@@ -662,9 +694,9 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async {
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
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");
if(await file.exists()) {
log("[${host ? 'HOST' : 'GAME'}] Path exists");
@@ -672,15 +704,19 @@ class _LaunchButtonState extends State<LaunchButton> {
}
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
if(customDll || isRetry) {
if(customDll) {
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
);
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _settingsController.downloadCriticalDllInteractive(file.path);
await _dllController.download(injectable, file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(injectable, host, true);
return _getDllFileOrStop(version, injectable, host, true);
}
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
@@ -691,32 +727,32 @@ class _LaunchButtonState extends State<LaunchButton> {
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
duration: null,
action: Obx(() {
if(_hostingController.started.value || linkedHosting) {
return const SizedBox.shrink();
}
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
duration: null,
action: Obx(() {
if(_hostingController.started.value || linkedHosting) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 2.0
),
child: Button(
onPressed: () async {
_backendController.joinLocalhost();
if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
_gameClientInfoBar?.close();
_showLaunchingGameClientWidget(version, hostType, true);
}
},
child: Text(translations.startGameServer),
),
);
})
);
return Padding(
padding: const EdgeInsets.only(
bottom: 2.0
),
child: Button(
onPressed: () async {
_backendController.joinLocalhost();
if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
_gameClientInfoBar?.close();
_showLaunchingGameClientWidget(version, hostType, true);
}
},
child: Text(translations.startGameServer),
),
);
})
);
}
}
@@ -731,7 +767,8 @@ enum _StopReason {
matchmakerError,
tokenError,
unknownError,
exitCode;
exitCode,
crash;
bool get isError => name.contains("Error");
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
@@ -18,9 +18,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
}
lastError = exception.toString();
final route = ModalRoute.of(pageKey.currentContext!);
if(route != null && !route.isCurrent){
Navigator.of(pageKey.currentContext!).pop(false);
if(inDialog){
final context = pageKey.currentContext;
if(context != null) {
Navigator.of(context).pop(false);
inDialog = false;
}
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(

View File

@@ -5,18 +5,19 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
import 'package:reboot_launcher/src/page/implementation/host_page.dart';
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/profile.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/widget/page/backend_page.dart';
import 'package:reboot_launcher/src/widget/page/home_page.dart';
import 'package:reboot_launcher/src/widget/page/host_page.dart';
import 'package:reboot_launcher/src/widget/page/play_page.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
void startOnboarding() {
final gameController = Get.find<GameController>();
final settingsController = Get.find<SettingsController>();
settingsController.firstRun.value = false;
profileOverlayKey.currentState!.showOverlay(
@@ -27,7 +28,7 @@ void startOnboarding() {
label: translations.startOnboardingActionLabel,
onTap: () async {
onClose();
await showProfileForm(context);
await showProfileForm(context, gameController.username, gameController.password);
_promptPlayPage();
}
)
@@ -62,7 +63,7 @@ void _promptPlayVersion() {
onTap: () async {
onClose();
if(!hasBuilds) {
await VersionSelector.openDownloadDialog(closable: false);
await VersionSelector.openDownloadDialog();
}
_promptServerBrowserPage();
}
@@ -78,6 +79,22 @@ void _promptServerBrowserPage() {
context: context,
label: translations.promptServerBrowserPageActionLabel,
onTap: () {
onClose();
_promptHostAccount();
}
)
);
}
void _promptHostAccount() {
pageIndex.value = RebootPageType.host.index;
profileOverlayKey.currentState!.showOverlay(
text: translations.hostAccountText,
offset: Offset(27.5, 17.5),
actionBuilder: (context, onClose) => _buildActionButton(
context: context,
label: translations.hostAccountAction,
onTap: () async {
onClose();
_promptHostPage();
}
@@ -86,7 +103,6 @@ void _promptServerBrowserPage() {
}
void _promptHostPage() {
pageIndex.value = RebootPageType.host.index;
pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptHostPageText,
actionBuilder: (context, onClose) => _buildActionButton(
@@ -339,7 +355,7 @@ Widget _buildActionButton({
required void Function() onTap,
}) => Button(
style: themed ? ButtonStyle(
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
) : null,
child: Text(label),
onPressed: onTap

View File

@@ -1,17 +1,15 @@
import 'package:email_validator/email_validator.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
final GameController _gameController = Get.find<GameController>();
Future<bool> showProfileForm(BuildContext context) async{
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
final showPassword = RxBool(false);
final oldUsername = _gameController.username.text;
final oldUsername = username.text;
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
final oldPassword = _gameController.password.text;
final oldPassword = password.text;
final result = await showRebootDialog<bool?>(
builder: (context) => Obx(() => FormDialog(
content: Column(
@@ -23,7 +21,18 @@ Future<bool> showProfileForm(BuildContext context) async{
label: translations.usernameOrEmail,
child: TextFormBox(
placeholder: translations.usernameOrEmailPlaceholder,
controller: _gameController.username,
validator: (text) {
if(password.text.isEmpty) {
return null;
}
if(EmailValidator.validate(username.text)) {
return null;
}
return translations.invalidEmail;
},
controller: username,
autovalidateMode: AutovalidateMode.always,
enableSuggestions: true,
autofocus: true,
@@ -35,7 +44,7 @@ Future<bool> showProfileForm(BuildContext context) async{
label: translations.password,
child: TextFormBox(
placeholder: translations.passwordPlaceholder,
controller: _gameController.password,
controller: password,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
@@ -44,8 +53,8 @@ Future<bool> showProfileForm(BuildContext context) async{
suffix: Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
showPassword.value ? Icons.visibility_off : Icons.visibility,
@@ -75,7 +84,7 @@ Future<bool> showProfileForm(BuildContext context) async{
return true;
}
_gameController.username.text = oldUsername;
_gameController.password.text = oldPassword;
username.text = oldUsername;
password.text = oldPassword;
return false;
}

View File

@@ -7,10 +7,11 @@ 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/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:universal_disk_space/universal_disk_space.dart';
import 'package:reboot_launcher/src/util/types.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:windows_taskbar/windows_taskbar.dart';
class AddVersionDialog extends StatefulWidget {
@@ -32,22 +33,14 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
final Rxn<FortniteBuild> _build = Rxn();
final RxnInt _timeLeft = RxnInt();
final Rxn<double> _progress = Rxn();
final RxInt _speed = RxInt(0);
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();
}
@@ -59,9 +52,9 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
}
void _cancelDownload() {
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
_downloadPort?.send(kStopBuildDownloadSignal);
_isolate?.kill(priority: Isolate.immediate);
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
stopDownloadServer();
}
@override
@@ -70,28 +63,10 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
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
));
}
);
return Obx(() => FormDialog(
content: _buildFormBody(downloadableBuilds),
buttons: _formButtons
));
case _DownloadStatus.downloading:
case _DownloadStatus.extracting:
return GenericDialog(
@@ -102,7 +77,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return ErrorDialog(
exception: _error ?? Exception(translations.unknownError),
stackTrace: _stackTrace,
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
errorMessageBuilder: (exception) {
var error = exception.toString();
error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error;
error = error.toLowerCase();
return translations.downloadVersionError(error);
}
);
case _DownloadStatus.done:
return InfoDialog(
@@ -151,7 +131,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
final communicationPort = ReceivePort();
communicationPort.listen((message) {
if(message is FortniteBuildDownloadProgress) {
_onProgress(build, message.progress, message.minutesLeft, message.extracting);
_onProgress(build, message);
}else if(message is SendPort) {
_downloadPort = message;
}else {
@@ -165,7 +145,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
);
final errorPort = ReceivePort();
errorPort.listen((message) => _onDownloadError(message, null));
_isolate = await Isolate.spawn(
await Isolate.spawn(
downloadArchiveBuild,
options,
onError: errorPort.sendPort,
@@ -205,23 +185,24 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
_stackTrace = stackTrace;
}
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) {
void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) {
if (!mounted) {
return;
}
if(progress >= 100 && extracting) {
if(message.progress >= 100 && message.extracting) {
_onDownloadComplete(build);
return;
}
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
if(progress >= 0) {
WindowsTaskbar.setProgress(progress.round(), 100);
_status.value = message.extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
if(message.progress >= 0) {
WindowsTaskbar.setProgress(message.progress.round(), 100);
}
_timeLeft.value = timeLeft;
_progress.value = progress;
_timeLeft.value = message.timeLeft;
_progress.value = message.progress;
_speed.value = message.speed;
}
Widget get _progressBody {
@@ -232,31 +213,33 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
Align(
alignment: Alignment.centerLeft,
child: Text(
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting,
_statusText,
style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start,
),
),
const SizedBox(
height: 8.0,
),
if(_progress.value != null)
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)
if(_progress.value != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
translations.timeLeft(timeLeft),
translations.buildProgress((_progress.value ?? 0).round()),
style: FluentTheme.maybeOf(context)?.typography.body,
)
],
),
),
if(timeLeft != null && timeLeft != -1)
Text(
translations.timeLeft(timeLeft),
style: FluentTheme.maybeOf(context)?.typography.body,
)
],
),
const SizedBox(
height: 8.0,
@@ -264,7 +247,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
SizedBox(
width: double.infinity,
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
child: ProgressBar(value: _progress.value?.toDouble())
),
const SizedBox(
@@ -274,7 +257,20 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
);
}
Widget _buildFormBody(List<FortniteBuild> builds) => Column(
String get _statusText {
if (_status.value != _DownloadStatus.downloading) {
return translations.extracting;
}
if (_progress.value == null) {
return translations.startingDownload;
}
return translations.downloading;
}
Widget _buildFormBody(List<FortniteBuild> builds) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -292,7 +288,8 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
controller: _pathController,
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
folder: true
folder: true,
allowNavigator: true
),
const SizedBox(
@@ -300,6 +297,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
)
],
);
}
String? _checkGameFolder(text) {
if (text == null || text.isEmpty) {
@@ -421,16 +419,15 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
_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 disks = WindowsDisk.available();
if(_source.value != _BuildSource.local && disks.isNotEmpty) {
final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
final build = _build.value;
if(build == null){
return;
}
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}";
_pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
}

View File

@@ -5,17 +5,16 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/server_start_button.dart';
import 'package:reboot_launcher/src/widget/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/server/server_start_button.dart';
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
@@ -43,7 +42,6 @@ class BackendPage extends RebootPage {
}
class _BackendPageState extends RebootPageState<BackendPage> {
final GameController _gameController = Get.find<GameController>();
final BackendController _backendController = Get.find<BackendController>();
InfoBarEntry? _infoBarEntry;
@@ -56,7 +54,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
}
if(keyEvent.physicalKey.isUnrealEngineKey) {
_gameController.consoleKey.value = keyEvent.physicalKey;
_backendController.consoleKey.value = keyEvent.physicalKey;
}
_infoBarEntry?.close();
@@ -154,9 +152,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
contentWidth: null,
content: Row(
children: [
Text(
Obx(() => Text(
_backendController.detached.value ? translations.on : translations.off
),
)),
const SizedBox(
width: 16.0
),
@@ -164,7 +162,12 @@ class _BackendPageState extends RebootPageState<BackendPage> {
key: backendDetachedOverlayTargetKey,
child: ToggleSwitch(
checked: _backendController.detached(),
onChanged: (value) => _backendController.detached.value = value
onChanged: (value) async {
_backendController.detached.value = value;
if(_backendController.started.value) {
await _backendController.restart();
}
}
),
),
],
@@ -194,7 +197,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
duration: null
);
},
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
child: Text(_backendController.consoleKey.value.unrealEnginePrettyName ?? ""),
),
)
);

View File

@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
@@ -9,12 +8,11 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
class BrowsePage extends RebootPage {
const BrowsePage({Key? key}) : super(key: key);
@@ -211,10 +209,18 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
icon: Icon(
hasPassword ? FluentIcons.lock : FluentIcons.globe
),
title: Text("${_formatName(entry)}${entry.author}"),
subtitle: Text("${_formatDescription(entry)}${_formatVersion(entry)}"),
title: Text(
"${_formatName(entry)}${entry.author}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
subtitle: Text(
"${_formatDescription(entry)}${_formatVersion(entry)}",
maxLines: 1,
overflow: TextOverflow.ellipsis
),
content: Button(
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry),
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
)
);
@@ -276,8 +282,8 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
_filterControllerStream.add("");
},
style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.transparent),
shape: ButtonState.all(Border())
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.all(Border())
),
child: _searchBarIconData
);

View File

@@ -3,36 +3,35 @@ import 'dart:io';
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/dll.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_suggestion.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart';
import 'package:reboot_launcher/src/widget/title_bar.dart';
import 'package:reboot_launcher/src/widget/window/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/fluent/profile_tile.dart';
import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart';
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
class HomePage extends StatefulWidget {
static const double kDefaultPadding = 12.0;
static const double kTitleBarHeight = 32;
const HomePage({Key? key}) : super(key: key);
@@ -42,8 +41,10 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
final BackendController _backendController = Get.find<BackendController>();
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
final GlobalKey _searchKey = GlobalKey();
final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController();
@@ -56,7 +57,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override
void initState() {
super.initState();
windowManager.setPreventClose(true);
windowManager.addListener(this);
_syncPageViewWithNavigator();
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -75,6 +75,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
lastPage = index;
_pageController.jumpToPage(index);
pagesController.add(null);
});
}
@@ -92,7 +93,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
final uuid = uri.host;
final server = _hostingController.findServerById(uuid);
if(server != null) {
_backendController.joinServerInteractive(_hostingController.uuid, server);
_backendController.joinServer(_hostingController.uuid, server);
}else {
showRebootInfoBar(
translations.noServerFound,
@@ -109,7 +110,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
return;
}
var result = await pingGameServer(address);
final result = await pingGameServer(address);
if(result) {
return;
}
@@ -133,28 +134,50 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
dllsDirectory.createSync(recursive: true);
}
for(final injectable in InjectableDll.values) {
final (file, custom) = _settingsController.getInjectableData(injectable);
if(!custom) {
_settingsController.downloadCriticalDllInteractive(
file.path,
silent: true
);
}
}
watchDlls().listen((filePath) => showDllDeletedDialog(() {
_settingsController.downloadCriticalDllInteractive(filePath);
}));
_dllController.guardFiles();
}
@override
void onWindowClose() async {
try {
await _hostingController.discardServer();
}finally {
exit(0); // Force closing
await windowManager.hide();
}catch(error) {
log("[WINDOW] Cannot hide window: $error");
}
try {
await _hostingController.discardServer();
}catch(error) {
log("[HOSTING] Cannot discard server on exit: $error");
}
try {
if(_backendController.started.value) {
await _backendController.toggle();
}
}catch(error) {
log("[BACKEND] Cannot stop backend on exit: $error");
}
try {
_gameController.instance.value?.kill();
}catch(error) {
log("[GAME] Cannot stop game on exit: $error");
}
try {
_hostingController.instance.value?.kill();
}catch(error) {
log("[HOST] Cannot stop host on exit: $error");
}
try {
await stopDownloadServer();
}catch(error) {
log("[ARIA] Cannot stop aria server on exit: $error");
}
exit(0);
}
@override
@@ -218,14 +241,18 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override
void onWindowResized() {
_settingsController.saveWindowSize(appWindow.size);
_focused.value = true;
windowManager.getSize().then((size) {
_settingsController.saveWindowSize(size);
});
}
@override
void onWindowMoved() {
_settingsController.saveWindowOffset(appWindow.position);
_focused.value = true;
windowManager.getPosition().then((position) {
_settingsController.saveWindowOffset(position);
});
}
@override
@@ -238,107 +265,79 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
_focused.value = true;
}
@override
void onWindowEvent(String eventName) {
if(eventName != "move") {
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
}
}
@override
Widget build(BuildContext context) {
super.build(context);
_settingsController.language.value;
loadTranslations(context);
return Obx(() {
return Container(
return Container(
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: HomePage.kTitleBarHeight,
child: Row(
children: [
_backButton,
Expanded(child: _draggableArea),
WindowTitleBar(focused: _focused())
],
)
),
Expanded(
child: Navigator(
key: appNavigatorKey,
onPopPage: (page, data) => false,
pages: [
MaterialPage(
child: Overlay(
key: appOverlayKey,
initialEntries: [
OverlayEntry(
maintainState: true,
builder: (context) => Row(
children: [
_buildLateralView(),
_buildBody()
],
)
)
],
),
child: Navigator(
key: appNavigatorKey,
onPopPage: (page, data) => false,
pages: [
MaterialPage(
child: Overlay(
key: appOverlayKey,
initialEntries: [
OverlayEntry(
maintainState: true,
builder: (context) => Row(
children: [
_buildLateralView(),
_buildBody()
],
)
)
],
)
),
)
],
),
);
});
}
Widget _buildBody() {
return Expanded(
child: Padding(
padding: EdgeInsets.only(
left: HomePage.kDefaultPadding,
right: HomePage.kDefaultPadding * 2,
top: HomePage.kDefaultPadding,
bottom: HomePage.kDefaultPadding * 2
),
child: Column(
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 1000
),
child: Center(
child: Column(
children: [
_buildBodyHeader(),
const SizedBox(height: 24.0),
Expanded(
child: Stack(
fit: StackFit.loose,
children: [
_buildBodyContent(),
InfoBarArea(
key: infoBarAreaKey
)
],
)
),
],
),
),
),
)
],
)
),
)
);
}
Widget _buildBody() => Expanded(
child: Padding(
padding: EdgeInsets.only(
left: HomePage.kDefaultPadding,
right: HomePage.kDefaultPadding * 2,
top: HomePage.kDefaultPadding,
bottom: HomePage.kDefaultPadding * 2
),
child: Column(
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 1000
),
child: Center(
child: Column(
children: [
_buildBodyHeader(),
const SizedBox(height: 24.0),
Expanded(
child: Stack(
fit: StackFit.loose,
children: [
_buildBodyContent(),
InfoBarArea(
key: infoBarAreaKey
)
],
)
),
],
),
),
),
)
],
)
),
);
Widget _buildBodyContent() => PageView.builder(
controller: _pageController,
itemBuilder: (context, index) => Navigator(
@@ -449,9 +448,12 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfileWidget(
overlayKey: profileOverlayKey
),
Obx(() {
pageIndex.value;
return ProfileWidget(
overlayKey: profileOverlayKey
);
}),
_autoSuggestBox,
const SizedBox(height: 12.0),
_buildNavigationTrail()
@@ -498,7 +500,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor(
FluentTheme.of(context),
pageIndex.value == index ? {ButtonStates.hovering} : states,
pageIndex.value == index ? {WidgetState.hovered} : states,
transparentWhenNone: true,
),
borderRadius: BorderRadius.all(Radius.circular(6.0))
@@ -523,42 +525,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
);
}
Widget get _backButton => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Button(
style: ButtonStyle(
padding: ButtonState.all(const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0
)),
backgroundColor: ButtonState.all(Colors.transparent),
shape: ButtonState.all(Border())
),
onPressed: appStack.isEmpty && !inDialog ? null : () {
if(inDialog) {
Navigator.of(appNavigatorKey.currentContext!).pop();
}else {
final lastPage = appStack.removeLast();
pageStack.remove(lastPage);
if (lastPage is int) {
hitBack = true;
pageIndex.value = lastPage;
} else {
Navigator.of(pageKey.currentContext!).pop();
}
}
pagesController.add(null);
},
child: const Icon(FluentIcons.back, size: 12.0),
)
);
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: appWindow.maximizeOrRestore,
onHorizontalDragStart: (_) => windowManager.startDragging(),
onVerticalDragStart: (_) => windowManager.startDragging()
);
Widget get _autoSuggestBox => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,

View File

@@ -7,20 +7,19 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
@@ -52,7 +51,7 @@ class HostPage extends RebootPage {
class _HostingPageState extends RebootPageState<HostPage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
@@ -80,10 +79,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
List<Widget> get settings => [
_information,
buildVersionSelector(
key: hostVersionOverlayTargetKey
key: hostVersionOverlayTargetKey
),
_options,
_internalFiles,
_share,
_resetDefaults
];
@@ -153,8 +151,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
suffix: Button(
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
@@ -173,9 +171,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: null,
content: Obx(() => Row(
children: [
Text(
Obx(() => Text(
_hostingController.discoverable.value ? translations.on : translations.off
),
)),
const SizedBox(
width: 16.0
),
@@ -199,6 +197,17 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.settingsServerOptionsName),
subtitle: Text(translations.settingsServerOptionsSubtitle),
children: [
SettingTile(
icon: Icon(
FluentIcons.options_24_regular
),
title: Text(translations.settingsClientArgsName),
subtitle: Text(translations.settingsClientArgsDescription),
content: TextFormBox(
placeholder: translations.settingsClientArgsPlaceholder,
controller: _hostingController.customLaunchArgs,
)
),
SettingTile(
icon: Icon(
FluentIcons.window_console_20_regular
@@ -224,9 +233,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: null,
content: Row(
children: [
Text(
Obx(() => Text(
_hostingController.autoRestart.value ? translations.on : translations.off
),
)),
const SizedBox(
width: 16.0
),
@@ -246,125 +255,17 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: 64,
content: TextFormBox(
placeholder: translations.settingsServerPortName,
controller: _settingsController.gameServerPort,
controller: _dllController.gameServerPort,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
]
)
),
)
],
);
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsServerName),
subtitle: Text(translations.settingsServerSubtitle),
children: [
SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTypeName),
subtitle: Text(translations.settingsServerTypeDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value),
onPressed: () {
final oldValue = _settingsController.customGameServer.value;
if(oldValue == entry.key) {
return;
}
_settingsController.customGameServer.value = entry.key;
_settingsController.infoBarEntry?.close();
if(!entry.key) {
_settingsController.updateReboot(
force: true
);
}
}
)).toList()
))
),
Obx(() {
if(!_settingsController.customGameServer.value) {
return const SizedBox.shrink();
}
return createFileSetting(
title: translations.settingsServerFileName,
description: translations.settingsServerFileDescription,
controller: _settingsController.gameServerDll
);
}),
Obx(() {
if(_settingsController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
content: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _settingsController.url,
validator: _checkUpdateUrl
)
);
}),
Obx(() {
if(_settingsController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_settingsController.timer.value = entry;
_settingsController.infoBarEntry?.close();
_settingsController.updateReboot(
force: true
);
}
)).toList()
))
);
}),
],
);
String? _checkUpdateUrl(String? text) {
if (text == null || text.isEmpty) {
return translations.emptyURL;
}
return null;
}
SettingTile get _share => SettingTile(
icon: Icon(
FluentIcons.link_24_regular
@@ -420,7 +321,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.hostResetName),
subtitle: Text(translations.hostResetDescription),
content: Button(
onPressed: () => showResetDialog(_hostingController.reset),
onPressed: () => showResetDialog(() {
_hostingController.reset();
_dllController.resetServer();
}),
child: Text(translations.hostResetContent),
)
);
@@ -432,8 +336,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
try {
_hostingController.publishServer(
_gameController.username.text,
_hostingController.instance.value!.versionName
_hostingController.accountUsername.text,
_hostingController.instance.value!.version.toString()
);
} catch(error) {
_showCannotUpdateGameServer(error);
@@ -467,14 +371,4 @@ class _HostingPageState extends RebootPageState<HostPage> {
severity: InfoBarSeverity.success,
duration: infoBarLongDuration
);
}
extension _UpdateTimerExtension on UpdateTimer {
String get text {
if (this == UpdateTimer.never) {
return translations.updateGameServerDllNever;
}
return translations.updateGameServerDllEvery(name);
}
}

View File

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

View File

@@ -0,0 +1,91 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
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 GameController _gameController = Get.find<GameController>();
final DllController _dllController = Get.find<DllController>();
@override
Widget? get button => LaunchButton(
startLabel: translations.launchFortnite,
stopLabel: translations.closeFortnite,
host: false
);
@override
List<SettingTile> get settings => [
buildVersionSelector(
key: gameVersionOverlayTargetKey
),
_options,
_resetDefaults
];
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,
)
)
]
);
SettingTile get _resetDefaults => SettingTile(
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.gameResetDefaultsName),
subtitle: Text(translations.gameResetDefaultsDescription),
content: Button(
onPressed: () => showResetDialog(() {
_gameController.reset();
_dllController.resetGame();
}),
child: Text(translations.gameResetDefaultsContent),
)
);
}

View File

@@ -0,0 +1,375 @@
import 'dart:math';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
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/dll_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/page/page.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/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>();
final DllController _dllController = Get.find<DllController>();
int? _downloadFromMirrorId;
@override
Widget? get button => null;
@override
List<Widget> get settings => [
_language,
_theme,
_internalFiles,
_installationDirectory,
];
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: _dllController.unrealEngineConsoleDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path;
_dllController.download(InjectableDll.console, path, force: true);
}
),
createFileSetting(
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
_dllController.backendDll.text = path;
_dllController.download(InjectableDll.auth, path, force: true);
}
),
createFileSetting(
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
_dllController.memoryLeakDll.text = path;
_dllController.download(InjectableDll.memoryLeak, path, force: true);
}
),
_internalFilesServerType,
_internalFilesUpdateTimer,
_internalFilesServerSource,
_internalFilesNewServerSource,
],
);
Widget get _internalFilesServerType => SettingTile(
icon: Icon(
FluentIcons.games_24_regular
),
title: Text(translations.settingsServerTypeName),
subtitle: Text(translations.settingsServerTypeDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value),
onPressed: () {
final oldValue = _dllController.customGameServer.value;
if(oldValue == entry.key) {
return;
}
_dllController.customGameServer.value = entry.key;
if(!entry.key) {
_dllController.updateGameServerDll(
force: true
);
}
}
)).toList()
))
);
Widget get _internalFilesServerSource => Obx(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerOldMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _dllController.updateGameServerDll(force: true),
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
)
);
}else {
return createFileSetting(
title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.customGameServerDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
_dllController.customGameServerDll.text = path;
_dllController.download(InjectableDll.gameServer, path);
}
);
}
});
void _scheduleMirrorDownload(String value) async {
if(_downloadFromMirrorId != null) {
return;
}
if(Uri.tryParse(value) == null) {
return;
}
final id = Random.secure().nextInt(1000000);
_downloadFromMirrorId = id;
await Future.delayed(const Duration(seconds: 2));
if(_downloadFromMirrorId == id) {
await _dllController.updateGameServerDll(force: true);
}
_downloadFromMirrorId = null;
}
Widget get _internalFilesNewServerSource => Obx(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerNewMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _dllController.updateGameServerDll(force: true),
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.aboveS20Mirror.text = kRebootBelowS20DownloadUrl;
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
)
);
}else {
return const SizedBox();
}
});
Widget get _internalFilesUpdateTimer => Obx(() {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_dllController.timer.value = entry;
_dllController.updateGameServerDll(
force: true
);
}
)).toList()
))
);
});
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 _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;
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
String get text {
if (this == UpdateTimer.never) {
return translations.updateGameServerDllNever;
}
return translations.updateGameServerDllEvery(name);
}
}

View File

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

View File

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

View File

@@ -1,51 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/title_bar_buttons.dart';
import 'package:system_theme/system_theme.dart';
class WindowTitleBar extends StatelessWidget {
final bool focused;
const WindowTitleBar({Key? key, required this.focused}) : super(key: key);
@override
Widget build(BuildContext context) {
var lightMode = FluentTheme.of(context).brightness.isLight;
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MinimizeWindowButton(
colors: WindowButtonColors(
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
iconMouseDown: lightMode ? Colors.black : Colors.white,
iconMouseOver: lightMode ? Colors.black : Colors.white,
normal: Colors.transparent,
mouseOver: _color,
mouseDown: _color.withOpacity(0.7)),
),
MaximizeWindowButton(
colors: WindowButtonColors(
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
iconMouseDown: lightMode ? Colors.black : Colors.white,
iconMouseOver: lightMode ? Colors.black : Colors.white,
normal: Colors.transparent,
mouseOver: _color,
mouseDown: _color.withOpacity(0.7)),
),
CloseWindowButton(
colors: WindowButtonColors(
iconNormal: focused || !isWin11 ? lightMode ? Colors.black : Colors.white : SystemTheme.accentColor.lighter,
iconMouseDown: lightMode ? Colors.black : Colors.white,
iconMouseOver: lightMode ? Colors.black : Colors.white,
normal: Colors.transparent,
mouseOver: Colors.red,
mouseDown: Colors.red.withOpacity(0.7),
),
),
],
);
}
Color get _color =>
SystemTheme.accentColor.accent;
}

View File

@@ -1,173 +0,0 @@
import 'package:bitsdojo_window/bitsdojo_window.dart' show appWindow;
import 'package:flutter/material.dart';
import 'title_bar_icons.dart';
import 'title_bar_mouse.dart';
typedef WindowButtonIconBuilder = Widget Function(
WindowButtonContext buttonContext);
typedef WindowButtonBuilder = Widget Function(
WindowButtonContext buttonContext, Widget icon);
class WindowButtonContext {
BuildContext context;
MouseState mouseState;
Color? backgroundColor;
Color iconColor;
WindowButtonContext(
{required this.context,
required this.mouseState,
this.backgroundColor,
required this.iconColor});
}
class WindowButtonColors {
late Color normal;
late Color mouseOver;
late Color mouseDown;
late Color iconNormal;
late Color iconMouseOver;
late Color iconMouseDown;
WindowButtonColors(
{Color? normal,
Color? mouseOver,
Color? mouseDown,
Color? iconNormal,
Color? iconMouseOver,
Color? iconMouseDown}) {
this.normal = normal ?? _defaultButtonColors.normal;
this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
}
}
final _defaultButtonColors = WindowButtonColors(
normal: Colors.transparent,
iconNormal: const Color(0xFF805306),
mouseOver: const Color(0xFF404040),
mouseDown: const Color(0xFF202020),
iconMouseOver: const Color(0xFFFFFFFF),
iconMouseDown: const Color(0xFFF0F0F0));
class WindowButton extends StatelessWidget {
final WindowButtonBuilder? builder;
final WindowButtonIconBuilder? iconBuilder;
late final WindowButtonColors colors;
final bool animate;
final EdgeInsets? padding;
final VoidCallback? onPressed;
WindowButton(
{Key? key,
WindowButtonColors? colors,
this.builder,
@required this.iconBuilder,
this.padding,
this.onPressed,
this.animate = false})
: super(key: key) {
this.colors = colors ?? _defaultButtonColors;
}
Color getBackgroundColor(MouseState mouseState) {
if (mouseState.isMouseDown) return colors.mouseDown;
if (mouseState.isMouseOver) return colors.mouseOver;
return colors.normal;
}
Color getIconColor(MouseState mouseState) {
if (mouseState.isMouseDown) return colors.iconMouseDown;
if (mouseState.isMouseOver) return colors.iconMouseOver;
return colors.iconNormal;
}
@override
Widget build(BuildContext context) {
return MouseStateBuilder(
builder: (context, mouseState) {
WindowButtonContext buttonContext = WindowButtonContext(
mouseState: mouseState,
context: context,
backgroundColor: getBackgroundColor(mouseState),
iconColor: getIconColor(mouseState));
var icon =
(iconBuilder != null) ? iconBuilder!(buttonContext) : Container();
var fadeOutColor =
getBackgroundColor(MouseState()..isMouseOver = true)
.withOpacity(0);
var padding = this.padding ?? EdgeInsets.zero;
var animationMs = mouseState.isMouseOver
? (animate ? 100 : 0)
: (animate ? 200 : 0);
Widget iconWithPadding = Padding(padding: padding, child: icon);
iconWithPadding = AnimatedContainer(
curve: Curves.easeOut,
duration: Duration(milliseconds: animationMs),
color: buttonContext.backgroundColor ?? fadeOutColor,
child: iconWithPadding);
var button = (builder != null)
? builder!(buttonContext, icon)
: iconWithPadding;
return SizedBox.square(dimension: 45, child: button);
},
onPressed: onPressed);
}
}
class MinimizeWindowButton extends WindowButton {
MinimizeWindowButton(
{Key? key,
WindowButtonColors? colors,
VoidCallback? onPressed,
bool? animate})
: super(
key: key,
colors: colors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MinimizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => appWindow.minimize());
}
class MaximizeWindowButton extends WindowButton {
MaximizeWindowButton(
{Key? key,
WindowButtonColors? colors,
VoidCallback? onPressed,
bool? animate})
: super(
key: key,
colors: colors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MaximizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ??
() => appWindow.maximizeOrRestore());
}
final _defaultCloseButtonColors = WindowButtonColors(
mouseOver: const Color(0xFFD32F2F),
mouseDown: const Color(0xFFB71C1C),
iconNormal: const Color(0xFF805306),
iconMouseOver: const Color(0xFFFFFFFF));
class CloseWindowButton extends WindowButton {
CloseWindowButton(
{Key? key,
WindowButtonColors? colors,
VoidCallback? onPressed,
bool? animate})
: super(
key: key,
colors: colors ?? _defaultCloseButtonColors,
animate: animate ?? false,
iconBuilder: (buttonContext) =>
CloseIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => appWindow.close());
}

View File

@@ -1,118 +0,0 @@
import 'dart:math';
import 'package:flutter/widgets.dart';
class CloseIcon extends StatelessWidget {
final Color color;
const CloseIcon({Key? key, required this.color}) : super(key: key);
@override
Widget build(BuildContext context) => Align(
alignment: Alignment.topLeft,
child: Stack(children: [
Transform.rotate(
angle: pi * .25,
child:
Center(child: Container(width: 14, height: 1, color: color))),
Transform.rotate(
angle: pi * -.25,
child:
Center(child: Container(width: 14, height: 1, color: color))),
]),
);
}
class MaximizeIcon extends StatelessWidget {
final Color color;
const MaximizeIcon({Key? key, required this.color}) : super(key: key);
@override
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
}
class _MaximizePainter extends _IconPainter {
_MaximizePainter(Color color) : super(color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
}
}
class RestoreIcon extends StatelessWidget {
final Color color;
const RestoreIcon({
Key? key,
required this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
}
class _RestorePainter extends _IconPainter {
_RestorePainter(Color color) : super(color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);
canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);
canvas.drawLine(
Offset(size.width, 0), Offset(size.width, size.height - 2), p);
canvas.drawLine(Offset(size.width, size.height - 2),
Offset(size.width - 2, size.height - 2), p);
}
}
class MinimizeIcon extends StatelessWidget {
final Color color;
const MinimizeIcon({Key? key, required this.color}) : super(key: key);
@override
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
}
class _MinimizePainter extends _IconPainter {
_MinimizePainter(Color color) : super(color);
@override
void paint(Canvas canvas, Size size) {
Paint p = getPaint(color);
canvas.drawLine(
Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
}
}
abstract class _IconPainter extends CustomPainter {
_IconPainter(this.color);
final Color color;
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class _AlignedPaint extends StatelessWidget {
const _AlignedPaint(this.painter, {Key? key}) : super(key: key);
final CustomPainter painter;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: CustomPaint(size: const Size(10, 10), painter: painter));
}
}
Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
..color = color
..style = PaintingStyle.stroke
..isAntiAlias = isAntiAlias
..strokeWidth = 1;

View File

@@ -1,71 +0,0 @@
import 'package:flutter/widgets.dart';
typedef MouseStateBuilderCB = Widget Function(
BuildContext context, MouseState mouseState);
class MouseState {
bool isMouseOver;
bool isMouseDown;
MouseState() : isMouseOver = false, isMouseDown = false;
}
class MouseStateBuilder extends StatefulWidget {
final MouseStateBuilderCB builder;
final VoidCallback? onPressed;
const MouseStateBuilder({Key? key, required this.builder, this.onPressed})
: super(key: key);
@override
State<MouseStateBuilder> createState() => _MouseStateBuilderState();
}
class _MouseStateBuilderState extends State<MouseStateBuilder> {
late MouseState _mouseState;
_MouseStateBuilderState() {
_mouseState = MouseState();
}
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (event) {
setState(() {
_mouseState.isMouseOver = true;
});
},
onExit: (event) {
setState(() {
_mouseState.isMouseOver = false;
});
},
child: GestureDetector(
onTapDown: (_) {
setState(() {
_mouseState.isMouseDown = true;
});
},
onTapCancel: () {
setState(() {
_mouseState.isMouseDown = false;
});
},
onTap: () {
setState(() {
_mouseState.isMouseDown = false;
_mouseState.isMouseOver = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.onPressed != null) {
widget.onPressed!();
}
});
},
onTapUp: (_) {},
child: widget.builder(context, _mouseState)
)
);
}
}

View File

@@ -6,20 +6,20 @@ import 'package:flutter/gestures.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/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/version.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/widget/message/version.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key);
static Future<void> openDownloadDialog({bool closable = true}) => showRebootDialog<bool>(
static Future<void> openDownloadDialog() => showRebootDialog<bool>(
builder: (context) => AddVersionDialog(
closable: closable,
closable: true,
),
dismissWithEsc: closable
dismissWithEsc: true
);
@override

View File

@@ -1,9 +1,9 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
SettingTile buildVersionSelector({
required GlobalKey<OverlayTargetState> key

View File

@@ -24,19 +24,22 @@ class InfoBarAreaState extends State<InfoBarArea> {
}
@override
Widget build(BuildContext context) => Obx(() => Padding(
padding: EdgeInsets.only(
bottom: hasPageButton ? 72.0 : 16.0
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: _children.value.map((child) => Padding(
padding: EdgeInsets.only(
top: 12.0
),
child: child
)).toList(growable: false)
),
));
Widget build(BuildContext context) => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Obx(() => Padding(
padding: EdgeInsets.only(
bottom: hasPageButton ? 72.0 : 16.0
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: _children.value.map((child) => Padding(
padding: EdgeInsets.only(
top: 12.0
),
child: child
)).toList(growable: false)
),
))
);
}

View File

@@ -1,11 +1,15 @@
name: reboot_launcher
description: Graphical User Interface for Project Reboot
version: "9.2.2"
version: "10.0.7"
publish_to: 'none'
environment:
sdk: ">=3.0.0 <=4.0.0"
# 3.19.0 is the last version that supports Windows 7/8/8.1 officially
# I have no clue who is still using Windows 7, but some users requested support, so might as well add it
# Repository Issue: https://github.com/Auties00/Reboot-Launcher/issues/58
# Flutter issue: https://github.com/flutter/flutter/issues/140830#issuecomment-1936397549
sdk: ">=3.0.0 <=3.19.0"
dependencies:
# The flutter SDK
@@ -17,53 +21,48 @@ dependencies:
path: ./../common
# Windows UI 3
fluent_ui: ^4.8.7
flutter_acrylic:
path: ./dependencies/flutter_acrylic
fluentui_system_icons: ^1.1.238
system_theme: ^2.0.0
fluent_ui: ^4.9.1
flutter_acrylic: ^1.1.4
fluentui_system_icons: ^1.1.258
system_theme: ^3.1.1
skeletons:
git:
url: https://github.com/talok/skeletons
ref: main
# Window management
bitsdojo_window: ^0.1.5
window_manager: ^0.3.8
window_manager: ^0.4.2
# Extract zip archives (for example the reboot.zip)
archive: ^3.3.1
archive: ^3.6.1
# Cryptographic functions
crypto: ^3.0.2
bcrypt: ^1.1.3
pointycastle: ^3.7.3
pointycastle: ^3.9.1
# Async helpers
async: ^2.8.2
async: ^2.11.0
sync: ^0.3.0
synchronized: ^3.3.0+3
# State management
get: ^4.6.5
get: ^4.6.6
# Native utilities
clipboard: ^0.1.3
app_links: ^6.0.2
url_protocol: ^1.0.0
app_links: ^6.3.2
windows_taskbar: ^1.1.2
file_picker: ^8.0.3
url_launcher: ^6.1.5
file_picker: ^8.1.2
url_launcher: ^6.3.0
local_notifier: ^0.1.6
# Server browser
supabase_flutter: ^2.5.2
uuid: ^3.0.6
supabase_flutter: ^2.7.0
dart_ipify: ^1.1.1
# Storage
get_storage: ^2.0.3
universal_disk_space: ^0.2.3
path: ^1.8.3
get_storage: ^2.1.1
path: ^1.9.0
# Translations
intl: any
@@ -71,20 +70,17 @@ dependencies:
# Auto updater
yaml: ^3.1.2
package_info_plus: ^8.0.0
package_info_plus: ^8.0.2
version: ^3.0.2
dependency_overrides:
xml: ^6.3.0
http: ^0.13.5
win32: ^3.0.0
ffi: ^2.0.0
# Validate profile
email_validator: ^3.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
@@ -98,4 +94,9 @@ flutter:
- assets/backend/profiles/
- assets/backend/public/
- assets/backend/responses/
- assets/backend/responses/Athena/
- assets/backend/responses/Athena/BattlePass/
- assets/backend/responses/Athena/Discovery/
- assets/backend/responses/Campaign/
- assets/backend/responses/CloudDir/
- assets/build/

View File

@@ -7,10 +7,9 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <local_notifier/local_notifier_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <system_theme/system_theme_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
@@ -19,14 +18,12 @@
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
FlutterAcrylicPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SystemThemePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SystemThemePlugin"));
UrlLauncherWindowsRegisterWithRegistrar(

View File

@@ -4,10 +4,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
bitsdojo_window_windows
flutter_acrylic
local_notifier
screen_retriever
screen_retriever_windows
system_theme
url_launcher_windows
window_manager

View File

@@ -36,7 +36,6 @@ Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
Source: "..\..\dependencies\redist\VC_redist.x64.exe"; DestDir: {tmp}; Flags: dontcopy
[Run]
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden
Filename: "{app}\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent
Filename: "{tmp}\VC_redist.x64.exe"; StatusMsg: "{cm:InstallingVC2017redist}"; Parameters: "/quiet"; Check: VC2017RedistNeedsInstall; Flags: waituntilterminated
@@ -46,6 +45,44 @@ Name: "{autodesktop}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; T
Name: "{userstartup}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup
[Code]
var
Page: TInputOptionWizardPage;
procedure InitializeWizard();
begin
Page := CreateInputOptionPage(
wpWelcome,
' Allow DLL injection',
' The Reboot Launcher needs to inject DLLs into Fortnite to create the game server',
'Selecting the option below will add the Reboot Launcher to the Windows Exclusions list. ' +
'This is necessary because DLL injection is often detected as a virus, but is necessary to modify Fortnite. ' +
'This option was designed for advanced users who want to manually manage the exclusions list on their machine. ' +
'If you do not trust the Reboot Launcher, you can audit the source code at https://github.com/Auties00/reboot_launcher and build it from source.',
False,
False
);
Page.Add('&Add the launcher to the Windows Exclusions list');
Page.Values[0] := True;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
InstallationDir: String;
begin
if (CurStep = ssPostInstall) and Page.Values[0] then
begin
InstallationDir := ExpandConstant('{app}');
Exec('powershell.exe', '-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath ''' + InstallationDir + '''""' , '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Log('Powershell exit code: ' + IntToStr(ResultCode));
end;
end;
function CompareVersion(version1, version2: String): Integer;
var
packVersion1, packVersion2: Int64;

View File

@@ -1,6 +1,3 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
#include <cstdlib>
#include <flutter/dart_project.h>

View File

@@ -6,6 +6,10 @@
#include <dwmapi.h>
#include <iostream>
#include "Windowsx.h"
namespace {
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
@@ -121,7 +125,7 @@ bool Win32Window::CreateAndShow(const std::wstring &title,
HWND window = CreateWindow(
window_class,
title.c_str(),
WS_OVERLAPPED | WS_BORDER | WS_THICKFRAME,
WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor),
Scale(origin.y, scale_factor),
Scale(size.width, scale_factor),
@@ -160,48 +164,44 @@ LRESULT CALLBACK Win32Window::WndProc(HWND const window,
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
Win32Window::MessageHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept {
switch (uMsg) {
case WM_DESTROY: {
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
}
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT *>(lparam);
auto newRectSize = reinterpret_cast<RECT *>(lParam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
SetWindowPos(hWnd, nullptr, newRectSize->left, newRectSize->top, newWidth,newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
auto rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,rect.bottom - rect.top, TRUE);
}
return 0;
return DefWindowProc(child_content_, uMsg, wParam, lParam);
}
case WM_ACTIVATE:
case WM_ACTIVATE: {
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
}
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
default:
return DefWindowProc(window_handle_, uMsg, wParam, lParam);
}
}
void Win32Window::Destroy() {
OnDestroy();
@@ -225,8 +225,7 @@ void Win32Window::SetChildContent(HWND content) {
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,frame.bottom - frame.top, true);
SetFocus(child_content_);
}