mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a775e2f3f | ||
|
|
2bf084d120 | ||
|
|
93c5d6c56b | ||
|
|
46034aa1fa | ||
|
|
3069f3aa05 |
21
README.md
21
README.md
@@ -1,9 +1,14 @@
|
||||
# Reboot Launcher
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
|
||||
Join our discord at https://discord.gg/reboot
|
||||
|
||||
## 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
|
||||
|
||||
## Installation
|
||||
|
||||
Check the releases section
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
import 'package:reboot_common/src/util/matchmaker.dart' as matchmaker;
|
||||
|
||||
late String? username;
|
||||
late bool host;
|
||||
@@ -82,7 +81,7 @@ void main(List<String> args) async {
|
||||
return;
|
||||
}
|
||||
|
||||
matchmaker.writeMatchmakingIp(result["matchmaking-address"]);
|
||||
writeMatchmakingIp(result["matchmaking-address"]);
|
||||
autoRestart = result["auto-restart"];
|
||||
await startGame();
|
||||
}
|
||||
@@ -24,7 +24,7 @@ Future<void> startGame() async {
|
||||
|
||||
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
|
||||
..exitCode.then((_) => _onClose())
|
||||
..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose));
|
||||
..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
|
||||
_injectOrShowError("cobalt.dll");
|
||||
}
|
||||
|
||||
@@ -52,6 +52,17 @@ void _onGameOutput(String line, String dll, bool hosting, bool 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;
|
||||
@@ -70,7 +81,7 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
||||
_injectOrShowError("console.dll");
|
||||
}
|
||||
|
||||
_injectOrShowError("memoryleak.dll");
|
||||
_injectOrShowError("memory.dll");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,12 +98,12 @@ Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
|
||||
|
||||
try {
|
||||
stdout.writeln("Injecting $binary...");
|
||||
var dll = locate ? File("${assetsDirectory.path}\\dlls\\$binary") : File(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.path);
|
||||
await injectDll(_gameProcess!.pid, dll);
|
||||
} catch (exception) {
|
||||
throw Exception("Cannot inject binary: $binary");
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ 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/memoryleak.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("${assetsDirectory.path}\\dlls\\console.dll");
|
||||
var consoleDll = File("${dllsDirectory.path}\\console.dll");
|
||||
if(!consoleDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_consoleDownload));
|
||||
if(response.statusCode != 200){
|
||||
@@ -22,7 +22,7 @@ Future<void> downloadRequiredDLLs() async {
|
||||
await consoleDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var craniumDll = File("${assetsDirectory.path}\\dlls\\cobalt.dll");
|
||||
var craniumDll = File("${dllsDirectory.path}\\cobalt.dll");
|
||||
if(!craniumDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_baseDownload));
|
||||
if(response.statusCode != 200){
|
||||
@@ -32,11 +32,11 @@ Future<void> downloadRequiredDLLs() async {
|
||||
await craniumDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var memoryFixDll = File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
|
||||
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 memoryleak.dll");
|
||||
throw Exception("Cannot download memory.dll");
|
||||
}
|
||||
|
||||
await memoryFixDll.writeAsBytes(response.bodyBytes);
|
||||
|
||||
@@ -10,6 +10,7 @@ export 'package:reboot_common/src/model/server_result.dart';
|
||||
export 'package:reboot_common/src/model/server_type.dart';
|
||||
export 'package:reboot_common/src/model/update_status.dart';
|
||||
export 'package:reboot_common/src/model/update_timer.dart';
|
||||
export 'package:reboot_common/src/model/dll.dart';
|
||||
export 'package:reboot_common/src/util/backend.dart';
|
||||
export 'package:reboot_common/src/util/build.dart';
|
||||
export 'package:reboot_common/src/util/dll.dart';
|
||||
|
||||
@@ -21,3 +21,4 @@ const List<String> kCannotConnectErrors = [
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
const String kGameFinishedLine = "PlayersLeft: 1";
|
||||
const String kDisplayInitializedLine = "Display";
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
extension ProcessExtension on Process {
|
||||
Stream<String> get stdOutput => this.stdout.expand((event) => utf8.decode(event).split("\n"));
|
||||
Stream<String> get stdOutput => this.stdout.expand((event) => utf8.decode(event, allowMalformed: true).split("\n"));
|
||||
|
||||
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event).split("\n"));
|
||||
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event, allowMalformed: true).split("\n"));
|
||||
}
|
||||
6
common/lib/src/model/dll.dart
Normal file
6
common/lib/src/model/dll.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
enum InjectableDll {
|
||||
console,
|
||||
cobalt,
|
||||
reboot,
|
||||
memory
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
|
||||
class GameInstance {
|
||||
final String versionName;
|
||||
final int gamePid;
|
||||
final int? launcherPid;
|
||||
final int? eacPid;
|
||||
final List<InjectableDll> injectedDlls;
|
||||
bool hosting;
|
||||
bool launched;
|
||||
bool movedToVirtualDesktop;
|
||||
@@ -19,7 +22,7 @@ class GameInstance {
|
||||
required this.eacPid,
|
||||
required this.hosting,
|
||||
required this.child
|
||||
}): tokenError = false, launched = false, movedToVirtualDesktop = false;
|
||||
}): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
|
||||
|
||||
void kill() {
|
||||
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
||||
|
||||
@@ -145,9 +145,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
'"${tempFile.path}"'
|
||||
],
|
||||
);
|
||||
var completed = false;
|
||||
process.stdOutput.listen((data) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if(data.toLowerCase().contains("everything is ok")) {
|
||||
completed = true;
|
||||
_onProgress(startTime, now, 100, true, options);
|
||||
process?.kill(ProcessSignal.sigabrt);
|
||||
return;
|
||||
@@ -166,6 +168,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
_onError(data, options);
|
||||
}
|
||||
});
|
||||
process.exitCode.then((_) {
|
||||
if(!completed) {
|
||||
_onError("Corrupted zip archive", options);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ".rar":
|
||||
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
|
||||
@@ -183,10 +190,12 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
'"${options.destination.path}"'
|
||||
]
|
||||
);
|
||||
var completed = false;
|
||||
process.stdOutput.listen((data) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
||||
if(data == "All OK") {
|
||||
completed = true;
|
||||
_onProgress(startTime, now, 100, true, options);
|
||||
process?.kill(ProcessSignal.sigabrt);
|
||||
return;
|
||||
@@ -205,6 +214,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
_onError(data, options);
|
||||
}
|
||||
});
|
||||
process.exitCode.then((_) {
|
||||
if(!completed) {
|
||||
_onError("Corrupted rar archive", options);
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError("Unexpected file extension: $extension}");
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
bool _watcher = false;
|
||||
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
|
||||
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
|
||||
const String kRebootDownloadUrl =
|
||||
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
|
||||
|
||||
@@ -18,7 +18,8 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
|
||||
}
|
||||
|
||||
Future<void> downloadCriticalDll(String name, String outputPath) async {
|
||||
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/assets/dlls/$name"));
|
||||
print("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name");
|
||||
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}");
|
||||
}
|
||||
|
||||
@@ -34,10 +34,11 @@ final class _MIB_TCPTABLE_OWNER_PID extends Struct {
|
||||
@Uint32()
|
||||
external int dwNumEntries;
|
||||
|
||||
@Array(1)
|
||||
@Array(512)
|
||||
external Array<_MIB_TCPROW_OWNER_PID> table;
|
||||
}
|
||||
|
||||
|
||||
bool isLocalHost(String host) => host.trim() == "127.0.0.1"
|
||||
|| host.trim().toLowerCase() == "localhost"
|
||||
|| host.trim() == "0.0.0.0";
|
||||
@@ -46,7 +47,6 @@ bool killProcessByPort(int port) {
|
||||
var pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>();
|
||||
final dwSize = calloc<DWORD>();
|
||||
dwSize.value = 0;
|
||||
|
||||
int result = _getExtendedTcpTable(
|
||||
nullptr,
|
||||
dwSize,
|
||||
@@ -56,6 +56,7 @@ bool killProcessByPort(int port) {
|
||||
0
|
||||
);
|
||||
if (result == ERROR_INSUFFICIENT_BUFFER) {
|
||||
free(pTcpTable);
|
||||
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
|
||||
result = _getExtendedTcpTable(
|
||||
pTcpTable,
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'dart:io';
|
||||
Directory get installationDirectory =>
|
||||
File(Platform.resolvedExecutable).parent;
|
||||
|
||||
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
|
||||
|
||||
Directory get assetsDirectory {
|
||||
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
|
||||
if(directory.existsSync()) {
|
||||
|
||||
@@ -33,7 +33,10 @@ final _CreateRemoteThread = _kernel32.lookupFunction<
|
||||
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
|
||||
const chunkSize = 1024;
|
||||
|
||||
Future<void> injectDll(int pid, String dll) async {
|
||||
Future<void> injectDll(int pid, File dll) async {
|
||||
// Get the path to the file
|
||||
final dllPath = dll.path;
|
||||
|
||||
final process = OpenProcess(
|
||||
0x43A,
|
||||
0,
|
||||
@@ -52,7 +55,7 @@ Future<void> injectDll(int pid, String dll) async {
|
||||
final dllAddress = VirtualAllocEx(
|
||||
process,
|
||||
nullptr,
|
||||
dll.length + 1,
|
||||
dllPath.length + 1,
|
||||
0x3000,
|
||||
0x4
|
||||
);
|
||||
@@ -60,8 +63,8 @@ Future<void> injectDll(int pid, String dll) async {
|
||||
final writeMemoryResult = WriteProcessMemory(
|
||||
process,
|
||||
dllAddress,
|
||||
dll.toNativeUtf8(),
|
||||
dll.length,
|
||||
dllPath.toNativeUtf8(),
|
||||
dllPath.length,
|
||||
nullptr
|
||||
);
|
||||
|
||||
@@ -89,6 +92,18 @@ Future<void> injectDll(int pid, String dll) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> startElevatedProcess({required String executable, required String args, bool window = false}) async {
|
||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
|
||||
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
||||
var shellResult = ShellExecuteEx(shellInput);
|
||||
return shellResult == 1;
|
||||
}
|
||||
|
||||
Future<Process> startProcess({required File executable, List<String>? args, bool wrapProcess = true, bool window = false, String? name}) async {
|
||||
final argsOrEmpty = args ?? [];
|
||||
if(wrapProcess) {
|
||||
@@ -223,6 +238,31 @@ List<String> createRebootArgs(String username, String password, bool host, bool
|
||||
return args;
|
||||
}
|
||||
|
||||
void handleGameOutput({
|
||||
required String line,
|
||||
required bool host,
|
||||
required void Function() onDisplayAttached,
|
||||
required void Function() onLoggedIn,
|
||||
required void Function() onMatchEnd,
|
||||
required void Function() onShutdown,
|
||||
required void Function() onTokenError,
|
||||
required void Function() onBuildCorrupted,
|
||||
}) {
|
||||
if (line.contains(kShutdownLine)) {
|
||||
onShutdown();
|
||||
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
|
||||
onBuildCorrupted();
|
||||
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
|
||||
onTokenError();
|
||||
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
||||
onLoggedIn();
|
||||
}else if(line.contains(kGameFinishedLine) && host) {
|
||||
onMatchEnd();
|
||||
}else if(line.contains(kDisplayInitializedLine) && host) {
|
||||
onDisplayAttached();
|
||||
}
|
||||
}
|
||||
|
||||
String _parseUsername(String username, bool host) {
|
||||
if(host) {
|
||||
return "Player${Random().nextInt(1000)}";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
Some Fortnite versions support running this game server in the background without rendering the game: this type of server is called "headless" as the game is running, but you can't see it on your screen.
|
||||
If headless is not supported by the Fortnite version you want to play, or if you disabled it manually from the "Configuration" section in the "Host" tab of the launcher, you will see an instance of Fortnite open on your screen.
|
||||
For convenience, this window will be opened on a new Virtual Desktop, if your Windows version supports it. This feature can be disabled as well from from the "Configuration" section in the "Host" tab of the launcher.
|
||||
Just like in Minecraft, you need a game client to play the game and one to host the server."
|
||||
Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.
|
||||
The project was started on Discord by Milxnor, while the launcher is developed by Auties00.
|
||||
Both are open source on GitHub, anyone can easily contribute or audit the code!"
|
||||
@@ -1,6 +1,6 @@
|
||||
A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features.
|
||||
By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github.
|
||||
If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue."
|
||||
If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue.
|
||||
LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend.
|
||||
Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user.
|
||||
You can run these alternatives either either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher.
|
||||
You can run these alternatives either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher.
|
||||
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck.exe
Normal file
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck.exe
Normal file
Binary file not shown.
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck_x64.exe
Normal file
BIN
gui/dependencies/InnoDependencyInstaller/netcorecheck_x64.exe
Normal file
Binary file not shown.
@@ -260,7 +260,7 @@
|
||||
"missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted",
|
||||
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version",
|
||||
"corruptedDllError": "Cannot inject dll: {error}",
|
||||
"tokenError": "Cannot log in into Fortnite: authentication error",
|
||||
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
|
||||
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
|
||||
"serverNoLongerAvailable": "{owner}'s server is no longer available",
|
||||
"serverNoLongerAvailableUnnamed": "The previous server is no longer available",
|
||||
|
||||
@@ -17,14 +17,14 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_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/info_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/error.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
|
||||
import 'package:reboot_launcher/src/util/info.dart';
|
||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||
import 'package:reboot_launcher/src/util/log.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';
|
||||
@@ -33,70 +33,107 @@ 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';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
const double kDefaultWindowWidth = 1536;
|
||||
const double kDefaultWindowHeight = 1024;
|
||||
const double kDefaultWindowWidth = 1164;
|
||||
const double kDefaultWindowHeight = 864;
|
||||
const String kCustomUrlSchema = "Reboot";
|
||||
|
||||
Version? appVersion;
|
||||
bool appWithNoStorage = false;
|
||||
|
||||
class _MyHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context){
|
||||
return super.createHttpClient(context)
|
||||
..badCertificateCallback = ((X509Certificate cert, String host, int port) => true);
|
||||
void main() {
|
||||
log("[APP] Called");
|
||||
runZonedGuarded(
|
||||
() => _startApp(),
|
||||
(error, stack) => onError(error, stack, false),
|
||||
zoneSpecification: ZoneSpecification(
|
||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startApp() async {
|
||||
final errors = <Object>[];
|
||||
try {
|
||||
log("[APP] Starting application");
|
||||
final pathError = await _initPath();
|
||||
if(pathError != null) {
|
||||
errors.add(pathError);
|
||||
}
|
||||
|
||||
final databaseError = await _initDatabase();
|
||||
if(databaseError != null) {
|
||||
errors.add(databaseError);
|
||||
}
|
||||
|
||||
final notificationsError = await _initNotifications();
|
||||
if(notificationsError != null) {
|
||||
errors.add(notificationsError);
|
||||
}
|
||||
|
||||
final tilesError = InfoPage.initInfoTiles();
|
||||
if(tilesError != null) {
|
||||
errors.add(tilesError);
|
||||
}
|
||||
|
||||
final versionError = await _initVersion();
|
||||
if(versionError != null) {
|
||||
errors.add(versionError);
|
||||
}
|
||||
|
||||
final storageErrors = await _initStorage();
|
||||
errors.addAll(storageErrors);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
_initWindow();
|
||||
|
||||
final urlError = await _initUrlHandler();
|
||||
if(urlError != null) {
|
||||
errors.add(urlError);
|
||||
}
|
||||
}catch(uncaughtError) {
|
||||
errors.add(uncaughtError);
|
||||
} finally{
|
||||
log("[APP] Started applications with errors: $errors");
|
||||
runApp(RebootApplication(
|
||||
errors: errors,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void main() => runZonedGuarded(
|
||||
() async {
|
||||
HttpOverrides.global = _MyHttpOverrides();
|
||||
final errors = <Object>[];
|
||||
try {
|
||||
await installationDirectory.create(recursive: true);
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrl,
|
||||
anonKey: supabaseAnonKey
|
||||
);
|
||||
await localNotifier.setup(
|
||||
appName: 'Reboot Launcher',
|
||||
shortcutPolicy: ShortcutPolicy.ignore
|
||||
);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemTheme.accentColor.load();
|
||||
_initWindow();
|
||||
initInfoTiles();
|
||||
final versionError = await _initVersion();
|
||||
if(versionError != null) {
|
||||
errors.add(versionError);
|
||||
}
|
||||
Future<Object?> _initNotifications() async {
|
||||
try {
|
||||
await localNotifier.setup(
|
||||
appName: 'Reboot Launcher',
|
||||
shortcutPolicy: ShortcutPolicy.ignore
|
||||
);
|
||||
return null;
|
||||
}catch(error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
final storageError = await _initStorage();
|
||||
if(storageError != null) {
|
||||
errors.add(storageError);
|
||||
}
|
||||
Future<Object?> _initDatabase() async {
|
||||
try {
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrl,
|
||||
anonKey: supabaseAnonKey
|
||||
);
|
||||
return null;
|
||||
}catch(error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
final urlError = await _initUrlHandler();
|
||||
if(urlError != null) {
|
||||
errors.add(urlError);
|
||||
}
|
||||
|
||||
_checkGameServer();
|
||||
}catch(uncaughtError) {
|
||||
errors.add(uncaughtError);
|
||||
} finally{
|
||||
runApp(const RebootApplication());
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(errors));
|
||||
}
|
||||
},
|
||||
(error, stack) => onError(error, stack, false),
|
||||
zoneSpecification: ZoneSpecification(
|
||||
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
|
||||
)
|
||||
);
|
||||
|
||||
void _handleErrors(List<Object?> errors) {
|
||||
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
|
||||
Future<Object?> _initPath() async {
|
||||
try {
|
||||
await installationDirectory.create(recursive: true);
|
||||
return null;
|
||||
}catch(error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Object?> _initVersion() async {
|
||||
@@ -109,116 +146,104 @@ Future<Object?> _initVersion() async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkGameServer() async {
|
||||
try {
|
||||
var backendController = Get.find<BackendController>();
|
||||
var address = backendController.gameServerAddress.text;
|
||||
if(isLocalHost(address)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await pingGameServer(address);
|
||||
if(result) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldOwner = backendController.gameServerOwner.value;
|
||||
backendController.joinLocalHost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
));
|
||||
}catch(_) {
|
||||
// Intended behaviour
|
||||
// Just ignore the error
|
||||
}
|
||||
}
|
||||
|
||||
Future<Object?> _initUrlHandler() async {
|
||||
try {
|
||||
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
|
||||
var appLinks = AppLinks();
|
||||
var initialUrl = await appLinks.getInitialLink();
|
||||
if(initialUrl != null) {
|
||||
_joinServer(initialUrl);
|
||||
}
|
||||
|
||||
appLinks.uriLinkStream.listen(_joinServer);
|
||||
return null;
|
||||
}catch(error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
void _joinServer(Uri uri) {
|
||||
var hostingController = Get.find<HostingController>();
|
||||
var backendController = Get.find<BackendController>();
|
||||
var uuid = _parseCustomUrl(uri);
|
||||
var server = hostingController.findServerById(uuid);
|
||||
if(server != null) {
|
||||
backendController.joinServer(hostingController.uuid, server);
|
||||
}else {
|
||||
showInfoBar(
|
||||
translations.noServerFound,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _parseCustomUrl(Uri uri) => uri.host;
|
||||
|
||||
void _initWindow() => doWhenWindowReady(() async {
|
||||
await windowManager.ensureInitialized();
|
||||
await Window.initialize();
|
||||
var settingsController = Get.find<SettingsController>();
|
||||
var size = Size(settingsController.width, settingsController.height);
|
||||
appWindow.size = size;
|
||||
var offsetX = settingsController.offsetX;
|
||||
var offsetY = settingsController.offsetY;
|
||||
if(offsetX != null && offsetY != null){
|
||||
appWindow.position = Offset(
|
||||
offsetX,
|
||||
offsetY
|
||||
);
|
||||
}else {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
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;
|
||||
var offsetX = settingsController.offsetX;
|
||||
var offsetY = settingsController.offsetY;
|
||||
if(offsetX != null && offsetY != null){
|
||||
appWindow.position = Offset(
|
||||
offsetX,
|
||||
offsetY
|
||||
);
|
||||
}else {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
|
||||
if(isWin11) {
|
||||
await Window.setEffect(
|
||||
effect: WindowEffect.acrylic,
|
||||
color: Colors.transparent,
|
||||
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
|
||||
);
|
||||
if(isWin11) {
|
||||
await Window.setEffect(
|
||||
effect: WindowEffect.acrylic,
|
||||
color: Colors.transparent,
|
||||
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
|
||||
);
|
||||
}
|
||||
}catch(error, stackTrace) {
|
||||
onError(error, stackTrace, false);
|
||||
}finally {
|
||||
appWindow.show();
|
||||
}
|
||||
|
||||
appWindow.show();
|
||||
});
|
||||
|
||||
Future<Object?> _initStorage() async {
|
||||
Future<List<Object>> _initStorage() async {
|
||||
final errors = <Object>[];
|
||||
try {
|
||||
await GetStorage("game", settingsDirectory.path).initStorage;
|
||||
await GetStorage("backend", settingsDirectory.path).initStorage;
|
||||
await GetStorage("update", settingsDirectory.path).initStorage;
|
||||
await GetStorage("settings", settingsDirectory.path).initStorage;
|
||||
await GetStorage("hosting", settingsDirectory.path).initStorage;
|
||||
Get.put(GameController());
|
||||
Get.put(BackendController());
|
||||
Get.put(BuildController());
|
||||
Get.put(SettingsController());
|
||||
Get.put(HostingController());
|
||||
Get.put(InfoController());
|
||||
Get.put(UpdateController());
|
||||
return null;
|
||||
}catch(error) {
|
||||
return error;
|
||||
appWithNoStorage = true;
|
||||
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(GameController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(BackendController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(BuildController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(HostingController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(UpdateController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
try {
|
||||
Get.put(SettingsController());
|
||||
}catch(error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
class RebootApplication extends StatefulWidget {
|
||||
const RebootApplication({Key? key}) : super(key: key);
|
||||
final List<Object> errors;
|
||||
const RebootApplication({Key? key, required this.errors}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RebootApplication> createState() => _RebootApplicationState();
|
||||
@@ -227,6 +252,16 @@ class RebootApplication extends StatefulWidget {
|
||||
class _RebootApplicationState extends State<RebootApplication> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(widget.errors));
|
||||
}
|
||||
|
||||
void _handleErrors(List<Object?> errors) {
|
||||
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() => FluentApp(
|
||||
locale: Locale(_settingsController.language.value),
|
||||
|
||||
@@ -5,9 +5,10 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
|
||||
class BackendController extends GetxController {
|
||||
late final GetStorage storage;
|
||||
late final GetStorage? storage;
|
||||
late final TextEditingController host;
|
||||
late final TextEditingController port;
|
||||
late final Rx<ServerType> type;
|
||||
@@ -21,13 +22,13 @@ class BackendController extends GetxController {
|
||||
HttpServer? remoteServer;
|
||||
|
||||
BackendController() {
|
||||
storage = GetStorage("backend");
|
||||
storage = appWithNoStorage ? null : GetStorage("backend");
|
||||
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);
|
||||
storage?.write("type", value.index);
|
||||
if (!started.value) {
|
||||
return;
|
||||
}
|
||||
@@ -36,13 +37,13 @@ class BackendController extends GetxController {
|
||||
});
|
||||
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));
|
||||
gameServerAddress = TextEditingController(text: storage.read("game_server_address") ?? "127.0.0.1");
|
||||
storage?.write("${type.value.name}_port", port.text));
|
||||
detached = RxBool(storage?.read("detached") ?? false);
|
||||
detached.listen((value) => storage?.write("detached", value));
|
||||
gameServerAddress = TextEditingController(text: storage?.read("game_server_address") ?? "127.0.0.1");
|
||||
var lastValue = gameServerAddress.text;
|
||||
writeMatchmakingIp(lastValue);
|
||||
gameServerAddress.addListener(() {
|
||||
@@ -53,7 +54,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) {
|
||||
@@ -62,15 +63,15 @@ class BackendController extends GetxController {
|
||||
}
|
||||
});
|
||||
gameServerAddressFocusNode = FocusNode();
|
||||
gameServerOwner = RxnString(storage.read("game_server_owner"));
|
||||
gameServerOwner.listen((value) => storage.write("game_server_owner", value));
|
||||
gameServerOwner = RxnString(storage?.read("game_server_owner"));
|
||||
gameServerOwner.listen((value) => storage?.write("game_server_owner", value));
|
||||
}
|
||||
|
||||
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);
|
||||
storage?.write("${type.name}_host", null);
|
||||
storage?.write("${type.name}_port", null);
|
||||
}
|
||||
|
||||
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
|
||||
@@ -79,7 +80,7 @@ class BackendController extends GetxController {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -92,7 +93,7 @@ class BackendController extends GetxController {
|
||||
}
|
||||
|
||||
String _readPort() =>
|
||||
storage.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
||||
storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
||||
|
||||
Stream<ServerResult> start() async* {
|
||||
try {
|
||||
|
||||
@@ -9,10 +9,12 @@ import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
|
||||
class GameController extends GetxController {
|
||||
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
|
||||
|
||||
late final GetStorage _storage;
|
||||
late final GetStorage? _storage;
|
||||
late final TextEditingController username;
|
||||
late final TextEditingController password;
|
||||
late final TextEditingController customLaunchArgs;
|
||||
@@ -23,38 +25,37 @@ class GameController extends GetxController {
|
||||
late final Rx<PhysicalKeyboardKey> consoleKey;
|
||||
|
||||
GameController() {
|
||||
_storage = GetStorage("game");
|
||||
Iterable decodedVersionsJson = jsonDecode(
|
||||
_storage.read("versions") ?? "[]");
|
||||
var decodedVersions = decodedVersionsJson
|
||||
_storage = appWithNoStorage ? null : GetStorage("game");
|
||||
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
|
||||
final decodedVersions = decodedVersionsJson
|
||||
.map((entry) => FortniteVersion.fromJson(entry))
|
||||
.toList();
|
||||
versions = Rx(decodedVersions);
|
||||
versions.listen((data) => _saveVersions());
|
||||
var decodedSelectedVersionName = _storage.read("version");
|
||||
var decodedSelectedVersion = decodedVersions.firstWhereOrNull((
|
||||
final decodedSelectedVersionName = _storage?.read("version");
|
||||
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((
|
||||
element) => element.name == decodedSelectedVersionName);
|
||||
_selectedVersion = Rxn(decodedSelectedVersion);
|
||||
username = TextEditingController(
|
||||
text: _storage.read("username") ?? kDefaultPlayerName);
|
||||
username.addListener(() => _storage.write("username", username.text));
|
||||
password = TextEditingController(text: _storage.read("password") ?? "");
|
||||
password.addListener(() => _storage.write("password", password.text));
|
||||
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? "");
|
||||
text: _storage?.read("username") ?? kDefaultPlayerName);
|
||||
username.addListener(() => _storage?.write("username", username.text));
|
||||
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));
|
||||
_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);
|
||||
_storage?.write("console_key", newValue.usbHidUsage);
|
||||
_writeConsoleKey(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
PhysicalKeyboardKey _readConsoleKey() {
|
||||
final consoleKeyValue = _storage.read("console_key");
|
||||
final consoleKeyValue = _storage?.read("console_key");
|
||||
if(consoleKeyValue == null) {
|
||||
return _kDefaultConsoleKey;
|
||||
}
|
||||
@@ -113,7 +114,7 @@ class GameController extends GetxController {
|
||||
|
||||
Future<void> _saveVersions() async {
|
||||
var serialized = jsonEncode(versions.value.map((entry) => entry.toJson()).toList());
|
||||
await _storage.write("versions", serialized);
|
||||
await _storage?.write("versions", serialized);
|
||||
}
|
||||
|
||||
bool get hasVersions => versions.value.isNotEmpty;
|
||||
@@ -124,7 +125,7 @@ class GameController extends GetxController {
|
||||
|
||||
set selectedVersion(FortniteVersion? version) {
|
||||
_selectedVersion.value = version;
|
||||
_storage.write("version", version?.name);
|
||||
_storage?.write("version", version?.name);
|
||||
}
|
||||
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||
|
||||
@@ -2,11 +2,12 @@ import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/main.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class HostingController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final GetStorage? _storage;
|
||||
late final String uuid;
|
||||
late final TextEditingController name;
|
||||
late final TextEditingController description;
|
||||
@@ -22,23 +23,23 @@ class HostingController extends GetxController {
|
||||
late final Rxn<Set<Map<String, dynamic>>> servers;
|
||||
|
||||
HostingController() {
|
||||
_storage = GetStorage("hosting");
|
||||
uuid = _storage.read("uuid") ?? const Uuid().v4();
|
||||
_storage.write("uuid", uuid);
|
||||
name = TextEditingController(text: _storage.read("name"));
|
||||
name.addListener(() => _storage.write("name", name.text));
|
||||
description = TextEditingController(text: _storage.read("description"));
|
||||
description.addListener(() => _storage.write("description", description.text));
|
||||
password = TextEditingController(text: _storage.read("password") ?? "");
|
||||
password.addListener(() => _storage.write("password", password.text));
|
||||
discoverable = RxBool(_storage.read("discoverable") ?? false);
|
||||
discoverable.listen((value) => _storage.write("discoverable", value));
|
||||
headless = RxBool(_storage.read("headless") ?? true);
|
||||
headless.listen((value) => _storage.write("headless", value));
|
||||
virtualDesktop = RxBool(_storage.read("virtual_desktop") ?? true);
|
||||
virtualDesktop.listen((value) => _storage.write("virtual_desktop", value));
|
||||
autoRestart = RxBool(_storage.read("auto_restart") ?? true);
|
||||
autoRestart.listen((value) => _storage.write("auto_restart", value));
|
||||
_storage = appWithNoStorage ? null : GetStorage("hosting");
|
||||
uuid = _storage?.read("uuid") ?? const Uuid().v4();
|
||||
_storage?.write("uuid", uuid);
|
||||
name = TextEditingController(text: _storage?.read("name"));
|
||||
name.addListener(() => _storage?.write("name", name.text));
|
||||
description = TextEditingController(text: _storage?.read("description"));
|
||||
description.addListener(() => _storage?.write("description", description.text));
|
||||
password = TextEditingController(text: _storage?.read("password") ?? "");
|
||||
password.addListener(() => _storage?.write("password", password.text));
|
||||
discoverable = RxBool(_storage?.read("discoverable") ?? false);
|
||||
discoverable.listen((value) => _storage?.write("discoverable", value));
|
||||
headless = RxBool(_storage?.read("headless") ?? true);
|
||||
headless.listen((value) => _storage?.write("headless", value));
|
||||
virtualDesktop = RxBool(_storage?.read("virtual_desktop") ?? true);
|
||||
virtualDesktop.listen((value) => _storage?.write("virtual_desktop", value));
|
||||
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
|
||||
autoRestart.listen((value) => _storage?.write("auto_restart", value));
|
||||
started = RxBool(false);
|
||||
published = RxBool(false);
|
||||
showPassword = RxBool(false);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class InfoController extends GetxController {
|
||||
List<String>? links;
|
||||
Map<String, String> linksData;
|
||||
|
||||
InfoController() : linksData = {};
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class SettingsController extends GetxController {
|
||||
gameServerDll = _createController("game_server", "reboot.dll");
|
||||
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
|
||||
backendDll = _createController("backend", "cobalt.dll");
|
||||
memoryLeakDll = _createController("memory_leak", "memoryleak.dll");
|
||||
memoryLeakDll = _createController("memory_leak", "memory.dll");
|
||||
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
|
||||
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
|
||||
width = _storage.read("width") ?? kDefaultWindowWidth;
|
||||
@@ -67,5 +67,5 @@ class SettingsController extends GetxController {
|
||||
firstRun.value = true;
|
||||
}
|
||||
|
||||
String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name";
|
||||
String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name";
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'package:version/version.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
class UpdateController {
|
||||
late final GetStorage _storage;
|
||||
late final GetStorage? _storage;
|
||||
late final RxnInt timestamp;
|
||||
late final Rx<UpdateStatus> status;
|
||||
late final Rx<UpdateTimer> timer;
|
||||
@@ -21,17 +21,17 @@ class UpdateController {
|
||||
Future? _updater;
|
||||
|
||||
UpdateController() {
|
||||
_storage = GetStorage("update");
|
||||
timestamp = RxnInt(_storage.read("ts"));
|
||||
timestamp.listen((value) => _storage.write("ts", value));
|
||||
var timerIndex = _storage.read("timer");
|
||||
_storage = appWithNoStorage ? null : GetStorage("update");
|
||||
timestamp = RxnInt(_storage?.read("ts"));
|
||||
timestamp.listen((value) => _storage?.write("ts", value));
|
||||
var 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));
|
||||
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));
|
||||
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
|
||||
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
|
||||
}
|
||||
|
||||
Future<void> notifyLauncherUpdate() async {
|
||||
@@ -65,17 +65,17 @@ class UpdateController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateReboot([bool force = false]) async {
|
||||
Future<void> updateReboot({bool force = false, bool silent = false}) async {
|
||||
if(_updater != null) {
|
||||
return await _updater;
|
||||
}
|
||||
|
||||
final result = _updateReboot(force);
|
||||
final result = _updateReboot(force, silent);
|
||||
_updater = result;
|
||||
return await result;
|
||||
}
|
||||
|
||||
Future<void> _updateReboot([bool force = false]) async {
|
||||
Future<void> _updateReboot(bool force, bool silent) async {
|
||||
try {
|
||||
if(customGameServer.value) {
|
||||
status.value = UpdateStatus.success;
|
||||
@@ -92,34 +92,44 @@ class UpdateController {
|
||||
return;
|
||||
}
|
||||
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadingDll("reboot"),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
timestamp.value = await downloadRebootDll(url.text);
|
||||
status.value = UpdateStatus.success;
|
||||
infoBarEntry?.close();
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
if(!silent) {
|
||||
infoBarEntry = showInfoBar(
|
||||
translations.downloadDllSuccess("reboot"),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showInfoBar(
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () => updateReboot(true),
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
if(!silent) {
|
||||
infoBarEntry?.close();
|
||||
var error = message.toString();
|
||||
error =
|
||||
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
status.value = UpdateStatus.error;
|
||||
showInfoBar(
|
||||
translations.downloadDllError("reboot.dll", error.toString()),
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error,
|
||||
action: Button(
|
||||
onPressed: () => updateReboot(
|
||||
force: true,
|
||||
silent: silent
|
||||
),
|
||||
child: Text(translations.downloadDllRetry),
|
||||
)
|
||||
);
|
||||
}
|
||||
}finally {
|
||||
_updater = null;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
import '../../util/log.dart';
|
||||
|
||||
|
||||
String? lastError;
|
||||
|
||||
void onError(Object exception, StackTrace? stackTrace, bool framework) {
|
||||
if(!kDebugMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
log("[ERROR] $exception");
|
||||
log("[STACKTRACE] $stackTrace");
|
||||
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ extension ServerControllerDialog on BackendController {
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
case ServerResultType.startError:
|
||||
print(event.stackTrace);
|
||||
return showInfoBar(
|
||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||
severity: InfoBarSeverity.error,
|
||||
|
||||
@@ -2,20 +2,26 @@ import 'dart:async';
|
||||
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/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/dialog/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/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/dll.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';
|
||||
@@ -33,6 +39,8 @@ class HomePage extends StatefulWidget {
|
||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
static const double _kDefaultPadding = 12.0;
|
||||
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final GlobalKey _searchKey = GlobalKey();
|
||||
@@ -45,15 +53,81 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
windowManager.addListener(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_updateController.notifyLauncherUpdate();
|
||||
_updateController.updateReboot();
|
||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||
downloadCriticalDllInteractive(filePath);
|
||||
}));
|
||||
_checkUpdates();
|
||||
_initAppLink();
|
||||
_checkGameServer();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _initAppLink() async {
|
||||
final appLinks = AppLinks();
|
||||
final initialUrl = await appLinks.getInitialLink();
|
||||
if(initialUrl != null) {
|
||||
_joinServer(initialUrl);
|
||||
}
|
||||
|
||||
appLinks.uriLinkStream.listen(_joinServer);
|
||||
}
|
||||
|
||||
void _joinServer(Uri uri) {
|
||||
final uuid = uri.host;
|
||||
final server = _hostingController.findServerById(uuid);
|
||||
if(server != null) {
|
||||
_backendController.joinServer(_hostingController.uuid, server);
|
||||
}else {
|
||||
showInfoBar(
|
||||
translations.noServerFound,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkGameServer() async {
|
||||
try {
|
||||
final address = _backendController.gameServerAddress.text;
|
||||
if(isLocalHost(address)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await pingGameServer(address);
|
||||
if(result) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldOwner = _backendController.gameServerOwner.value;
|
||||
_backendController.joinLocalHost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
));
|
||||
}catch(_) {
|
||||
// Intended behaviour
|
||||
// Just ignore the error
|
||||
}
|
||||
}
|
||||
|
||||
void _checkUpdates() {
|
||||
_updateController.notifyLauncherUpdate();
|
||||
|
||||
if(!dllsDirectory.existsSync()) {
|
||||
dllsDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
for(final injectable in InjectableDll.values) {
|
||||
downloadCriticalDllInteractive(
|
||||
injectable.path,
|
||||
silent: true
|
||||
);
|
||||
}
|
||||
|
||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||
downloadCriticalDllInteractive(filePath);
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -147,54 +221,55 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
super.build(context);
|
||||
_settingsController.language.value;
|
||||
loadTranslations(context);
|
||||
return Obx(() => NavigationPaneTheme(
|
||||
data: NavigationPaneThemeData(
|
||||
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
),
|
||||
child: NavigationView(
|
||||
paneBodyBuilder: (pane, body) => _PaneBody(
|
||||
padding: _kDefaultPadding,
|
||||
controller: pagesController,
|
||||
body: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
height: 32,
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: pageIndex.value,
|
||||
onChanged: (index) {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
menuButton: const SizedBox(),
|
||||
displayMode: PaneDisplayMode.open,
|
||||
items: _items,
|
||||
customPane: _CustomPane(_settingsController),
|
||||
header: const ProfileWidget(),
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
indicator: const StickyNavigationIndicator(
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeOut,
|
||||
indicatorSize: 3.25
|
||||
)
|
||||
),
|
||||
contentShape: const RoundedRectangleBorder(),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
)
|
||||
),
|
||||
);
|
||||
return Obx(() {
|
||||
return NavigationPaneTheme(
|
||||
data: NavigationPaneThemeData(
|
||||
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
),
|
||||
child: NavigationView(
|
||||
paneBodyBuilder: (pane, body) => _PaneBody(
|
||||
padding: _kDefaultPadding,
|
||||
controller: pagesController,
|
||||
body: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
height: 32,
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: pageIndex.value,
|
||||
onChanged: (index) {
|
||||
final lastPageIndex = pageIndex.value;
|
||||
if(lastPageIndex != index) {
|
||||
pageIndex.value = index;
|
||||
}else if(pageStack.isNotEmpty) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
pagesController.add(null);
|
||||
}
|
||||
},
|
||||
menuButton: const SizedBox(),
|
||||
displayMode: PaneDisplayMode.open,
|
||||
items: _items,
|
||||
customPane: _CustomPane(_settingsController),
|
||||
header: const ProfileWidget(),
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
indicator: const StickyNavigationIndicator(
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeOut,
|
||||
indicatorSize: 3.25
|
||||
)
|
||||
),
|
||||
contentShape: const RoundedRectangleBorder(),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _backButton => StreamBuilder(
|
||||
|
||||
@@ -1,15 +1,53 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:flutter/foundation.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/page/abstract/page.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/info.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||
|
||||
class InfoPage extends RebootPage {
|
||||
static late final List<InfoTile> _infoTiles;
|
||||
static Object? initInfoTiles() {
|
||||
try {
|
||||
final directory = Directory("${assetsDirectory.path}\\info\\$currentLocale");
|
||||
final map = SplayTreeMap<int, InfoTile>();
|
||||
for(final entry in directory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
map[index] = InfoTile(
|
||||
title: Text(questionName),
|
||||
content: Text(entry.readAsStringSync())
|
||||
);
|
||||
}
|
||||
}
|
||||
_infoTiles = map.values.toList(growable: false);
|
||||
return null;
|
||||
}catch(error) {
|
||||
_infoTiles = [];
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -30,10 +68,12 @@ class InfoPage extends RebootPage {
|
||||
|
||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
RxInt _counter = RxInt(180);
|
||||
RxInt _counter = RxInt(kDebugMode ? 0 : 180);
|
||||
late bool _showButton;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_showButton = _settingsController.firstRun.value;
|
||||
if(_settingsController.firstRun.value) {
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_counter.value <= 0) {
|
||||
@@ -48,27 +88,32 @@ class _InfoPageState extends RebootPageState<InfoPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> get settings => infoTiles;
|
||||
List<Widget> get settings => InfoPage._infoTiles;
|
||||
|
||||
@override
|
||||
Widget? get button => Obx(() {
|
||||
if(!_settingsController.firstRun.value) {
|
||||
Widget? get button {
|
||||
if(!_showButton) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final totalSecondsLeft = _counter.value;
|
||||
final minutesLeft = totalSecondsLeft ~/ 60;
|
||||
final secondsLeft = totalSecondsLeft % 60;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: totalSecondsLeft <= 0 ? () => pageIndex.value = RebootPageType.play.index : null,
|
||||
child: Text(
|
||||
totalSecondsLeft <= 0 ? "I have read the instructions"
|
||||
: "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}"
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
return Obx(() {
|
||||
final totalSecondsLeft = _counter.value;
|
||||
final minutesLeft = totalSecondsLeft ~/ 60;
|
||||
final secondsLeft = totalSecondsLeft % 60;
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: totalSecondsLeft <= 0 ? () {
|
||||
_showButton = false;
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
} : null,
|
||||
child: Text(
|
||||
totalSecondsLeft <= 0 ? "I have read the instructions"
|
||||
: "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}"
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
@@ -65,8 +66,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget get button => const LaunchButton(
|
||||
host: true
|
||||
Widget get button => LaunchButton(
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -194,6 +197,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
title: Text(translations.settingsServerTypeName),
|
||||
subtitle: Text(translations.settingsServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
@@ -209,7 +214,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
_updateController.customGameServer.value = entry.key;
|
||||
_updateController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_updateController.updateReboot(true);
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
@@ -256,13 +263,17 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_updateController.timer.value = entry;
|
||||
_updateController.infoBarEntry?.close();
|
||||
_updateController.updateReboot(true);
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
|
||||
@@ -5,6 +5,7 @@ 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/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
@@ -46,6 +47,8 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
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)),
|
||||
@@ -60,6 +63,8 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
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),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -11,60 +13,95 @@ import 'package:reboot_launcher/src/util/translations.dart';
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final Map<String, Future<void>> _operations = {};
|
||||
|
||||
Future<void> downloadCriticalDllInteractive(String filePath) {
|
||||
Future<void> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
|
||||
final old = _operations[filePath];
|
||||
if(old != null) {
|
||||
return old;
|
||||
}
|
||||
|
||||
final newRun = _downloadCriticalDllInteractive(filePath);
|
||||
final newRun = _downloadCriticalDllInteractive(filePath, silent);
|
||||
_operations[filePath] = newRun;
|
||||
return newRun;
|
||||
}
|
||||
|
||||
Future<void> _downloadCriticalDllInteractive(String filePath) async {
|
||||
Future<void> _downloadCriticalDllInteractive(String filePath, bool silent) async {
|
||||
final fileName = path.basename(filePath).toLowerCase();
|
||||
InfoBarEntry? entry;
|
||||
try {
|
||||
if (fileName == "reboot.dll") {
|
||||
await _updateController.updateReboot(true);
|
||||
await _updateController.updateReboot(
|
||||
silent: silent
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(File(filePath).existsSync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
|
||||
entry = showInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
if(!silent) {
|
||||
entry = showInfoBar(
|
||||
translations.downloadingDll(fileNameWithoutExtension),
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
}
|
||||
await downloadCriticalDll(fileName, filePath);
|
||||
entry.close();
|
||||
entry = await showInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}catch(message) {
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showInfoBar(
|
||||
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;
|
||||
if(!silent) {
|
||||
entry = await showInfoBar(
|
||||
translations.downloadDllSuccess(fileNameWithoutExtension),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarShortDuration
|
||||
);
|
||||
}
|
||||
}catch(message) {
|
||||
if(!silent) {
|
||||
entry?.close();
|
||||
var error = message.toString();
|
||||
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
|
||||
error = error.toLowerCase();
|
||||
final completer = Completer();
|
||||
await showInfoBar(
|
||||
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;
|
||||
}
|
||||
}finally {
|
||||
_operations.remove(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
extension InjectableDllExtension on InjectableDll {
|
||||
String get path {
|
||||
final SettingsController settingsController = Get.find<SettingsController>();
|
||||
switch(this){
|
||||
case InjectableDll.reboot:
|
||||
if(_updateController.customGameServer.value) {
|
||||
final file = File(settingsController.gameServerDll.text);
|
||||
if(file.existsSync()) {
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
return rebootDllFile.path;
|
||||
case InjectableDll.console:
|
||||
return settingsController.unrealEngineConsoleDll.text;
|
||||
case InjectableDll.cobalt:
|
||||
return settingsController.backendDll.text;
|
||||
case InjectableDll.memory:
|
||||
return settingsController.memoryLeakDll.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/util/log.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||
|
||||
final _entries = SplayTreeMap<int, InfoTile>();
|
||||
|
||||
void initInfoTiles() {
|
||||
try {
|
||||
final directory = Directory("${assetsDirectory.path}\\info\\$currentLocale");
|
||||
for(final entry in directory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
_entries[index] = InfoTile(
|
||||
title: Text(questionName),
|
||||
content: Text(entry.readAsStringSync())
|
||||
);
|
||||
}
|
||||
}
|
||||
}catch(error) {
|
||||
log("[INFO] Error occurred while initializing info tiles: $error");
|
||||
}
|
||||
}
|
||||
|
||||
List<InfoTile> get infoTiles => _entries.values.toList(growable: false);
|
||||
@@ -17,7 +17,13 @@ File _createLoggingFile() {
|
||||
}
|
||||
|
||||
void log(String message) async {
|
||||
await _semaphore.acquire();
|
||||
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
_semaphore.release();
|
||||
try {
|
||||
await _semaphore.acquire();
|
||||
print(message);
|
||||
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
|
||||
}catch(error) {
|
||||
print(error);
|
||||
}finally {
|
||||
_semaphore.release();
|
||||
}
|
||||
}
|
||||
@@ -305,10 +305,25 @@ final class Win32Process extends Struct {
|
||||
external int HWndLength;
|
||||
|
||||
external Pointer<Uint32> HWnd;
|
||||
|
||||
external Pointer<Utf16> excluded;
|
||||
}
|
||||
|
||||
int _filter(int HWnd, int lParam) {
|
||||
final structure = Pointer.fromAddress(lParam).cast<Win32Process>();
|
||||
if(structure.ref.excluded != nullptr) {
|
||||
final excludedWindowName = structure.ref.excluded.toDartString();
|
||||
final windowNameLength = GetWindowTextLength(HWnd);
|
||||
if(windowNameLength > 0) {
|
||||
final windowNamePointer = calloc<Uint16>(windowNameLength + 1).cast<Utf16>();
|
||||
GetWindowText(HWnd, windowNamePointer, windowNameLength);
|
||||
final windowName = windowNamePointer.toDartString(length: windowNameLength);
|
||||
if(windowName.toLowerCase().contains(excludedWindowName.toLowerCase())) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final pidPointer = calloc<Uint32>();
|
||||
GetWindowThreadProcessId(HWnd, pidPointer);
|
||||
final pid = pidPointer.value;
|
||||
@@ -330,9 +345,13 @@ int _filter(int HWnd, int lParam) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
List<int> _getHWnds(int pid) {
|
||||
List<int> _getHWnds(int pid, String? excludedWindowName) {
|
||||
final result = calloc<Win32Process>();
|
||||
result.ref.pid = pid;
|
||||
if(excludedWindowName != null) {
|
||||
result.ref.excluded = excludedWindowName.toNativeUtf16();
|
||||
}
|
||||
|
||||
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
|
||||
final length = result.ref.HWndLength;
|
||||
final HWndsPointer = result.ref.HWnd;
|
||||
@@ -400,24 +419,26 @@ class VirtualDesktopManager {
|
||||
|
||||
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
|
||||
|
||||
Future<void> moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1)}) async {
|
||||
final hWNDs = _getHWnds(pid);
|
||||
if(hWNDs.isEmpty) {
|
||||
await Future.delayed(pollTime);
|
||||
await moveWindowToDesktop(pid, desktop, pollTime: pollTime);
|
||||
return;
|
||||
}
|
||||
|
||||
for(final hWND in hWNDs) {
|
||||
Future<bool> moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1), int remainingPolls = 10, String? excludedWindowName}) async {
|
||||
for(final hWND in _getHWnds(pid, excludedWindowName)) {
|
||||
final window = applicationViewCollection.getViewForHWnd(hWND);
|
||||
if(window != null) {
|
||||
windowManager.moveWindowToDesktop(window, desktop);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(remainingPolls <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await Future.delayed(pollTime);
|
||||
await moveWindowToDesktop(pid, desktop, pollTime: pollTime);
|
||||
return await moveWindowToDesktop(
|
||||
pid,
|
||||
desktop,
|
||||
pollTime: pollTime,
|
||||
remainingPolls: remainingPolls - 1
|
||||
);
|
||||
}
|
||||
|
||||
IVirtualDesktop createDesktop() => windowManager.createDesktop();
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:dart_ipify/dart_ipify.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:path/path.dart';
|
||||
@@ -12,7 +13,6 @@ 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/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
@@ -27,10 +27,10 @@ import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class LaunchButton extends StatefulWidget {
|
||||
final bool host;
|
||||
final String? startLabel;
|
||||
final String? stopLabel;
|
||||
final String startLabel;
|
||||
final String stopLabel;
|
||||
|
||||
const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel}) : super(key: key);
|
||||
const LaunchButton({Key? key, required this.host, required this.startLabel, required this.stopLabel}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LaunchButton> createState() => _LaunchButtonState();
|
||||
@@ -43,7 +43,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
InfoBarEntry? _gameClientInfoBar;
|
||||
InfoBarEntry? _gameServerInfoBar;
|
||||
CancelableOperation? _operation;
|
||||
@@ -60,52 +59,42 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
onPressed: () => _operation = CancelableOperation.fromFuture(_toggle()),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(_hasStarted ? _stopMessage : _startMessage)
|
||||
child: Text((widget.host ? _hostingController.started() : _gameController.started()) ? widget.stopLabel : widget.startLabel)
|
||||
)
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
bool get _hasStarted => widget.host ? _hostingController.started() : _gameController.started();
|
||||
|
||||
void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started;
|
||||
|
||||
String get _startMessage => widget.startLabel ?? (widget.host ? translations.startHosting : translations.startGame);
|
||||
|
||||
String get _stopMessage => widget.stopLabel ?? (widget.host ? translations.stopHosting : translations.stopGame);
|
||||
|
||||
Future<void> _toggle({bool forceGUI = false}) async {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
|
||||
if (_hasStarted) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
Future<void> _toggle({bool? host, bool forceGUI = false}) async {
|
||||
host ??= widget.host;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
|
||||
if (host ? _hostingController.started() : _gameController.started()) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
|
||||
_onStop(
|
||||
reason: _StopReason.normal
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(_operation != null) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Already started, ignoring user action");
|
||||
return;
|
||||
}
|
||||
|
||||
final version = _gameController.selectedVersion;
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Version data: $version");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
|
||||
if(version == null){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] No version selected");
|
||||
log("[${host ? 'HOST' : 'GAME'}] No version selected");
|
||||
_onStop(
|
||||
reason: _StopReason.missingVersionError
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Setting started...");
|
||||
_setStarted(widget.host, true);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Set started");
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking dlls: ${_Injectable.values}");
|
||||
for (final injectable in _Injectable.values) {
|
||||
if(await _getDllFileOrStop(injectable, widget.host) == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Setting started...");
|
||||
_setStarted(host, true);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -113,7 +102,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
try {
|
||||
final executable = version.gameExecutable;
|
||||
if(executable == null){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] No executable found");
|
||||
log("[${host ? 'HOST' : 'GAME'}] No executable found");
|
||||
_onStop(
|
||||
reason: _StopReason.missingExecutableError,
|
||||
error: version.location.path
|
||||
@@ -121,27 +110,27 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
|
||||
final backendResult = _backendController.started() || await _backendController.toggleInteractive();
|
||||
if(!backendResult){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Cannot start backend");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
|
||||
_onStop(
|
||||
reason: _StopReason.backendError
|
||||
);
|
||||
return;
|
||||
}
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Backend works");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Backend works");
|
||||
final headless = !forceGUI && _hostingController.headless.value;
|
||||
final virtualDesktop = _hostingController.virtualDesktop.value;
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, headless, virtualDesktop, false);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||
await _startGameProcesses(version, widget.host, headless, virtualDesktop, linkedHostingInstance);
|
||||
if(!widget.host) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
|
||||
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, virtualDesktop, false);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
|
||||
await _startGameProcesses(version, host, headless, virtualDesktop, linkedHostingInstance);
|
||||
if(!host) {
|
||||
_showLaunchingGameClientWidget();
|
||||
}
|
||||
|
||||
if(linkedHostingInstance != null || widget.host){
|
||||
if(linkedHostingInstance != null || host){
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
} catch (exception, stackTrace) {
|
||||
@@ -153,34 +142,34 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool headless, bool virtualDesktop, bool forceLinkedHosting) async {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
||||
if(widget.host){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
||||
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool virtualDesktop, bool forceLinkedHosting) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
|
||||
if(host){
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_backendController.type.value != ServerType.embedded || !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_hostingController.started()){
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] The user has already manually started the hosting server");
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user has already manually started the hosting server");
|
||||
return null;
|
||||
}
|
||||
|
||||
final response = forceLinkedHosting || await _askForAutomaticGameServer();
|
||||
if(!response) {
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
||||
log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Starting implicit game server...");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
|
||||
final instance = await _startGameProcesses(version, true, headless, virtualDesktop, null);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Started implicit game server...");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
|
||||
_setStarted(true, true);
|
||||
log("[${widget.host ? 'HOST' : 'GAME'}] Set implicit game server as started");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -239,17 +228,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}else{
|
||||
_gameController.instance.value = instance;
|
||||
}
|
||||
await _injectOrShowError(_Injectable.sslBypassV2, host);
|
||||
await _injectOrShowError(InjectableDll.cobalt, host);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
|
||||
return instance;
|
||||
}
|
||||
|
||||
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async {
|
||||
if(!_hasStarted) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Discarding start game process request as the state is no longer started");
|
||||
return null;
|
||||
}
|
||||
|
||||
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
|
||||
final gameArgs = createRebootArgs(
|
||||
_gameController.username.text,
|
||||
@@ -265,40 +249,52 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
wrapProcess: false,
|
||||
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
||||
);
|
||||
gameProcess.stdOutput.listen((line) => _onGameOutput(line, version, host, virtualDesktop, false));
|
||||
gameProcess.stdError.listen((line) => _onGameOutput(line, version, host, virtualDesktop, true));
|
||||
watchProcess(gameProcess.pid).then((_) async {
|
||||
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),
|
||||
onLoggedIn: () =>_onLoggedIn(host),
|
||||
onMatchEnd: () => _onMatchEnd(version, virtualDesktop),
|
||||
onDisplayAttached: () => _onDisplayAttached(headless, virtualDesktop, version)
|
||||
);
|
||||
}
|
||||
gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
|
||||
gameProcess.stdError.listen((line) => onGameOutput(line, true));
|
||||
gameProcess.exitCode.then((_) async {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance == null) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!host || !headless || instance.launched) {
|
||||
_onStop(reason: _StopReason.exitCode);
|
||||
if(!host || instance.launched) {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): stop signal");
|
||||
_onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
host: host
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _restartGameServer(version, virtualDesktop, _StopReason.exitCode);
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): restart signal");
|
||||
instance.launched = true;
|
||||
await _onStop(
|
||||
reason: _StopReason.exitCode,
|
||||
host: true
|
||||
);
|
||||
await _toggle(
|
||||
forceGUI: true,
|
||||
host: true
|
||||
);
|
||||
});
|
||||
return gameProcess.pid;
|
||||
}
|
||||
|
||||
Future<void> _restartGameServer(FortniteVersion version, bool virtualDesktop, _StopReason reason) async {
|
||||
if (widget.host) {
|
||||
await _onStop(reason: reason);
|
||||
_toggle(forceGUI: true);
|
||||
} else {
|
||||
await _onStop(reason: reason, host: true);
|
||||
final linkedHostingInstance =
|
||||
await _startMatchMakingServer(version, false, virtualDesktop, true);
|
||||
_gameController.instance.value?.child = linkedHostingInstance;
|
||||
if (linkedHostingInstance != null) {
|
||||
_setStarted(true, true);
|
||||
_showLaunchingGameServerWidget();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async {
|
||||
if (file == null) {
|
||||
return null;
|
||||
@@ -314,74 +310,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return pid;
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, FortniteVersion version, bool host, bool virtualDesktop, bool error) async {
|
||||
if (line.contains(kShutdownLine)) {
|
||||
_onStop(
|
||||
reason: _StopReason.normal
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(kCorruptedBuildErrors.any((element) => line.contains(element))){
|
||||
_onStop(
|
||||
reason: _StopReason.corruptedVersionError
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(kCannotConnectErrors.any((element) => line.contains(element))){
|
||||
_onStop(
|
||||
reason: _StopReason.tokenError
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance != null && !instance.launched) {
|
||||
instance.launched = true;
|
||||
instance.tokenError = false;
|
||||
await _injectOrShowError(_Injectable.memoryFix, host);
|
||||
if(!host){
|
||||
await _injectOrShowError(_Injectable.console, host);
|
||||
_onGameClientInjected();
|
||||
}else {
|
||||
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text);
|
||||
if(gameServerPort != null) {
|
||||
await killProcessByPort(gameServerPort);
|
||||
}
|
||||
await _injectOrShowError(_Injectable.reboot, host);
|
||||
_onGameServerInjected();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains(kGameFinishedLine) && host) {
|
||||
if(_hostingController.autoRestart.value) {
|
||||
final notification = LocalNotification(
|
||||
title: translations.gameServerEnd,
|
||||
body: translations.gameServerRestart(_kRebootDelay.inSeconds),
|
||||
);
|
||||
notification.show();
|
||||
Future.delayed(_kRebootDelay).then((_) {
|
||||
_restartGameServer(version, virtualDesktop, _StopReason.normal);
|
||||
});
|
||||
}else {
|
||||
final notification = LocalNotification(
|
||||
title: translations.gameServerEnd,
|
||||
body: translations.gameServerShutdown(_kRebootDelay.inSeconds)
|
||||
);
|
||||
notification.show();
|
||||
Future.delayed(_kRebootDelay).then((_) {
|
||||
_onStop(reason: _StopReason.normal, host: true);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Display") && host && virtualDesktop) {
|
||||
Future<void> _onDisplayAttached(bool headless, bool virtualDesktop, FortniteVersion version) async {
|
||||
if(!headless && virtualDesktop) {
|
||||
final hostingInstance = _hostingController.instance.value;
|
||||
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
|
||||
hostingInstance.movedToVirtualDesktop = true;
|
||||
@@ -389,10 +319,18 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
final windowManager = VirtualDesktopManager.getInstance();
|
||||
_virtualDesktop = windowManager.createDesktop();
|
||||
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
|
||||
var success = false;
|
||||
try {
|
||||
await windowManager.moveWindowToDesktop(hostingInstance.gamePid, _virtualDesktop!);
|
||||
success = await windowManager.moveWindowToDesktop(
|
||||
hostingInstance.gamePid,
|
||||
_virtualDesktop!,
|
||||
excludedWindowName: "Reboot"
|
||||
);
|
||||
}catch(error) {
|
||||
log("[VIRTUAL_DESKTOP] $error");
|
||||
success = false;
|
||||
}
|
||||
if(!success) {
|
||||
try {
|
||||
windowManager.removeDesktop(_virtualDesktop!);
|
||||
}catch(error) {
|
||||
@@ -408,6 +346,63 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onMatchEnd(FortniteVersion version, bool virtualDesktop) {
|
||||
if(_hostingController.autoRestart.value) {
|
||||
final notification = LocalNotification(
|
||||
title: translations.gameServerEnd,
|
||||
body: translations.gameServerRestart(_kRebootDelay.inSeconds),
|
||||
);
|
||||
notification.show();
|
||||
Future.delayed(_kRebootDelay).then((_) async {
|
||||
log("[RESTARTER] Stopping server...");
|
||||
await _onStop(
|
||||
reason: _StopReason.normal,
|
||||
host: true
|
||||
);
|
||||
log("[RESTARTER] Stopped server");
|
||||
log("[RESTARTER] Starting server...");
|
||||
await _toggle(
|
||||
host: true
|
||||
);
|
||||
log("[RESTARTER] Started server");
|
||||
});
|
||||
}else {
|
||||
final notification = LocalNotification(
|
||||
title: translations.gameServerEnd,
|
||||
body: translations.gameServerShutdown(_kRebootDelay.inSeconds)
|
||||
);
|
||||
notification.show();
|
||||
Future.delayed(_kRebootDelay).then((_) {
|
||||
log("[RESTARTER] Stopping server...");
|
||||
_onStop(
|
||||
reason: _StopReason.normal,
|
||||
host: true
|
||||
);
|
||||
log("[RESTARTER] Stopped server");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoggedIn(bool host) async {
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance != null && !instance.launched) {
|
||||
instance.launched = true;
|
||||
instance.tokenError = false;
|
||||
await _injectOrShowError(InjectableDll.memory, host);
|
||||
if(!host){
|
||||
await _injectOrShowError(InjectableDll.console, host);
|
||||
_onGameClientInjected();
|
||||
}else {
|
||||
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text);
|
||||
if(gameServerPort != null) {
|
||||
await killProcessByPort(gameServerPort);
|
||||
}
|
||||
await _injectOrShowError(InjectableDll.reboot, host);
|
||||
_onGameServerInjected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onGameClientInjected() {
|
||||
_gameClientInfoBar?.close();
|
||||
showInfoBar(
|
||||
@@ -427,11 +422,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
duration: null
|
||||
);
|
||||
final gameServerPort = _settingsController.gameServerPort.text;
|
||||
_gameServerInfoBar?.close();
|
||||
final localPingResult = await pingGameServer(
|
||||
"127.0.0.1:$gameServerPort",
|
||||
timeout: const Duration(minutes: 2)
|
||||
);
|
||||
_gameServerInfoBar?.close();
|
||||
if (!localPingResult) {
|
||||
showInfoBar(
|
||||
translations.gameServerStartWarning,
|
||||
@@ -440,7 +435,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_backendController.joinLocalHost();
|
||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||
if (!accessible) {
|
||||
@@ -503,6 +497,20 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
|
||||
if(host == null) {
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
await _backendController.worker?.cancel();
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(host){
|
||||
_hostingController.instance.value = null;
|
||||
}else {
|
||||
_gameController.instance.value = null;
|
||||
}
|
||||
|
||||
if(_virtualDesktop != null) {
|
||||
try {
|
||||
final instance = VirtualDesktopManager.getInstance();
|
||||
@@ -512,20 +520,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
if(host == null) {
|
||||
await _operation?.cancel();
|
||||
_operation = null;
|
||||
await _backendController.worker?.cancel();
|
||||
}
|
||||
|
||||
host = host ?? widget.host;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
|
||||
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
|
||||
if(host) {
|
||||
_hostingController.discardServer();
|
||||
}
|
||||
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance != null) {
|
||||
if(reason == _StopReason.normal) {
|
||||
instance.launched = true;
|
||||
@@ -534,25 +534,21 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
instance.kill();
|
||||
final child = instance.child;
|
||||
if(child != null) {
|
||||
_onStop(
|
||||
await _onStop(
|
||||
reason: reason,
|
||||
host: child.hosting
|
||||
);
|
||||
}
|
||||
|
||||
if(host){
|
||||
_hostingController.instance.value = null;
|
||||
}else {
|
||||
_gameController.instance.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
_setStarted(host, false);
|
||||
if(host) {
|
||||
_gameServerInfoBar?.close();
|
||||
}else {
|
||||
_gameClientInfoBar?.close();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(host == true) {
|
||||
_gameServerInfoBar?.close();
|
||||
}else {
|
||||
_gameClientInfoBar?.close();
|
||||
}
|
||||
});
|
||||
|
||||
switch(reason) {
|
||||
case _StopReason.backendError:
|
||||
@@ -574,7 +570,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
);
|
||||
break;
|
||||
case _StopReason.exitCode:
|
||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if(instance != null && !instance.launched) {
|
||||
showInfoBar(
|
||||
translations.corruptedVersionError,
|
||||
@@ -582,7 +577,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case _StopReason.corruptedVersionError:
|
||||
showInfoBar(
|
||||
@@ -600,7 +594,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
break;
|
||||
case _StopReason.tokenError:
|
||||
showInfoBar(
|
||||
translations.tokenError,
|
||||
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration,
|
||||
);
|
||||
@@ -615,7 +609,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _injectOrShowError(_Injectable injectable, bool hosting) async {
|
||||
Future<void> _injectOrShowError(InjectableDll injectable, bool hosting) async {
|
||||
final instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
|
||||
if (instance == null) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] No instance found to inject ${injectable.name}");
|
||||
@@ -637,7 +631,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
|
||||
await injectDll(gameProcess, dllPath.path);
|
||||
await injectDll(gameProcess, dllPath);
|
||||
instance.injectedDlls.add(injectable);
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
|
||||
} catch (error, stackTrace) {
|
||||
log("[${hosting ? 'HOST' : 'GAME'}] Cannot inject ${injectable.name}: $error $stackTrace");
|
||||
@@ -650,29 +645,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
String _getDllPath(_Injectable injectable) {
|
||||
switch(injectable){
|
||||
case _Injectable.reboot:
|
||||
if(_updateController.customGameServer.value) {
|
||||
final file = File(_settingsController.gameServerDll.text);
|
||||
if(file.existsSync()) {
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
return rebootDllFile.path;
|
||||
case _Injectable.console:
|
||||
return _settingsController.unrealEngineConsoleDll.text;
|
||||
case _Injectable.sslBypassV2:
|
||||
return _settingsController.backendDll.text;
|
||||
case _Injectable.memoryFix:
|
||||
return _settingsController.memoryLeakDll.text;
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> _getDllFileOrStop(_Injectable injectable, bool host) async {
|
||||
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host) async {
|
||||
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
|
||||
final path = _getDllPath(injectable);
|
||||
final path = injectable.path;
|
||||
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
|
||||
final file = File(path);
|
||||
if(await file.exists()) {
|
||||
@@ -712,11 +687,4 @@ enum _StopReason {
|
||||
exitCode;
|
||||
|
||||
bool get isError => name.contains("Error");
|
||||
}
|
||||
|
||||
enum _Injectable {
|
||||
console,
|
||||
sslBypassV2,
|
||||
reboot,
|
||||
memoryFix,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
|
||||
class ServerTypeSelector extends StatefulWidget {
|
||||
@@ -18,6 +19,7 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
leading: Text(_controller.type.value.label),
|
||||
items: ServerType.values
|
||||
.map((type) => _createItem(type))
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/add_server_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
@@ -39,16 +40,22 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Obx(() {
|
||||
return _createOptionsMenu(
|
||||
version: _gameController.selectedVersion,
|
||||
close: false,
|
||||
child: FlyoutTarget(
|
||||
controller: _flyoutController,
|
||||
child: DropDownButton(
|
||||
leading: Text(_gameController.selectedVersion?.name ?? translations.selectVersion),
|
||||
items: _createSelectorItems(context)
|
||||
),
|
||||
)
|
||||
);
|
||||
version: _gameController.selectedVersion,
|
||||
close: false,
|
||||
child: FlyoutTarget(
|
||||
controller: _flyoutController,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_gameController.selectedVersion?.name ?? translations.selectVersion,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
items: _createSelectorItems(context)
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
|
||||
@@ -10,5 +10,11 @@ SettingTile get versionSelectSettingTile => SettingTile(
|
||||
),
|
||||
title: Text(translations.selectFortniteName),
|
||||
subtitle: Text(translations.selectFortniteDescription),
|
||||
content: const VersionSelector()
|
||||
contentWidth: null,
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: SettingTile.kDefaultContentWidth,
|
||||
),
|
||||
child: const VersionSelector()
|
||||
)
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
name: reboot_launcher
|
||||
description: Graphical User Interface for Project Reboot
|
||||
version: "9.1.0"
|
||||
version: "9.1.3"
|
||||
|
||||
publish_to: 'none'
|
||||
|
||||
@@ -90,7 +90,6 @@ flutter:
|
||||
uses-material-design: true
|
||||
generate: true
|
||||
assets:
|
||||
- assets/dlls/
|
||||
- assets/icons/
|
||||
- assets/images/
|
||||
- assets/backend/
|
||||
|
||||
@@ -10,10 +10,10 @@ AppPublisher={{PUBLISHER_NAME}}
|
||||
AppPublisherURL={{PUBLISHER_URL}}
|
||||
AppSupportURL={{PUBLISHER_URL}}
|
||||
AppUpdatesURL={{PUBLISHER_URL}}
|
||||
DefaultDirName={{INSTALL_DIR_NAME}}
|
||||
DefaultDirName={autopf}\{{DISPLAY_NAME}}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputBaseFilename={{OUTPUT_BASE_FILENAME}}
|
||||
Compression=lzma
|
||||
Compression=zip
|
||||
SolidCompression=yes
|
||||
SetupIconFile={{SETUP_ICON_FILE}}
|
||||
WizardStyle=modern
|
||||
@@ -28,11 +28,15 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
|
||||
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Dirs]
|
||||
Name: "{app}"; Permissions: everyone-full
|
||||
|
||||
[Files]
|
||||
Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Permissions: everyone-full
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent
|
||||
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
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"
|
||||
|
||||
Reference in New Issue
Block a user