mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
9.0.8
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
export 'package:reboot_common/src/constant/backend.dart';
|
export 'package:reboot_common/src/constant/backend.dart';
|
||||||
export 'package:reboot_common/src/constant/game.dart';
|
export 'package:reboot_common/src/constant/game.dart';
|
||||||
export 'package:reboot_common/src/constant/matchmaker.dart';
|
|
||||||
export 'package:reboot_common/src/constant/supabase.dart';
|
export 'package:reboot_common/src/constant/supabase.dart';
|
||||||
export 'package:reboot_common/src/model/fortnite_build.dart';
|
export 'package:reboot_common/src/model/fortnite_build.dart';
|
||||||
export 'package:reboot_common/src/model/fortnite_version.dart';
|
export 'package:reboot_common/src/model/fortnite_version.dart';
|
||||||
@@ -12,8 +11,9 @@ export 'package:reboot_common/src/model/update_timer.dart';
|
|||||||
export 'package:reboot_common/src/util/backend.dart';
|
export 'package:reboot_common/src/util/backend.dart';
|
||||||
export 'package:reboot_common/src/util/build.dart';
|
export 'package:reboot_common/src/util/build.dart';
|
||||||
export 'package:reboot_common/src/util/dll.dart';
|
export 'package:reboot_common/src/util/dll.dart';
|
||||||
export 'package:reboot_common/src/util/matchmaker.dart';
|
|
||||||
export 'package:reboot_common/src/util/network.dart';
|
export 'package:reboot_common/src/util/network.dart';
|
||||||
export 'package:reboot_common/src/util/patcher.dart';
|
export 'package:reboot_common/src/util/patcher.dart';
|
||||||
export 'package:reboot_common/src/util/path.dart';
|
export 'package:reboot_common/src/util/path.dart';
|
||||||
export 'package:reboot_common/src/util/process.dart';
|
export 'package:reboot_common/src/util/process.dart';
|
||||||
|
export 'package:reboot_common/src/extension/path.dart';
|
||||||
|
export 'package:reboot_common/src/extension/process.dart';
|
||||||
@@ -20,3 +20,4 @@ const List<String> kCannotConnectErrors = [
|
|||||||
"Network failure when attempting to check platform restrictions",
|
"Network failure when attempting to check platform restrictions",
|
||||||
"UOnlineAccountCommon::ForceLogout"
|
"UOnlineAccountCommon::ForceLogout"
|
||||||
];
|
];
|
||||||
|
const String kGameFinishedLine = "PlayersLeft: 1";
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
const String kDefaultMatchmakerHost = "127.0.0.1";
|
|
||||||
const int kDefaultMatchmakerPort = 8080;
|
|
||||||
42
common/lib/src/extension/path.dart
Normal file
42
common/lib/src/extension/path.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
import 'package:reboot_common/common.dart';
|
||||||
|
|
||||||
|
extension FortniteVersionExtension on FortniteVersion {
|
||||||
|
static File? findExecutable(Directory directory, String name) {
|
||||||
|
try{
|
||||||
|
final result = directory.listSync(recursive: true)
|
||||||
|
.firstWhere((element) => path.basename(element.path) == name);
|
||||||
|
return File(result.path);
|
||||||
|
}catch(_){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File? get gameExecutable => findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||||
|
|
||||||
|
Future<File?> get headlessGameExecutable async {
|
||||||
|
final result = findExecutable(location, "FortniteClient-Win64-Shipping-Headless.exe");
|
||||||
|
if(result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||||
|
if(original == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Headless.exe");
|
||||||
|
await original.copy(output.path);
|
||||||
|
await Isolate.run(() => patchHeadless(output));
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
|
||||||
|
|
||||||
|
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||||
|
|
||||||
|
File? get splashBitmap => findExecutable(location, "Splash.bmp");
|
||||||
|
}
|
||||||
8
common/lib/src/extension/process.dart
Normal file
8
common/lib/src/extension/process.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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 stdError => this.stderr.expand((event) => utf8.decode(event).split("\n"));
|
||||||
|
}
|
||||||
15
common/lib/src/extension/types.dart
Normal file
15
common/lib/src/extension/types.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
extension StringExtension on String {
|
||||||
|
bool get isBlank {
|
||||||
|
if(isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(var char in this.split("")) {
|
||||||
|
if(char != " ") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,12 @@ class FortniteBuild {
|
|||||||
final String identifier;
|
final String identifier;
|
||||||
final String version;
|
final String version;
|
||||||
final String link;
|
final String link;
|
||||||
final FortniteBuildSource source;
|
|
||||||
|
|
||||||
FortniteBuild({required this.identifier, required this.version, required this.link, required this.source});
|
FortniteBuild({
|
||||||
}
|
required this.identifier,
|
||||||
|
required this.version,
|
||||||
enum FortniteBuildSource {
|
required this.link
|
||||||
manifest,
|
});
|
||||||
archive
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FortniteBuildDownloadProgress {
|
class FortniteBuildDownloadProgress {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class GameInstance {
|
|||||||
final int? eacPid;
|
final int? eacPid;
|
||||||
bool hosting;
|
bool hosting;
|
||||||
bool launched;
|
bool launched;
|
||||||
|
bool movedToVirtualDesktop;
|
||||||
bool tokenError;
|
bool tokenError;
|
||||||
GameInstance? child;
|
GameInstance? child;
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ class GameInstance {
|
|||||||
required this.eacPid,
|
required this.eacPid,
|
||||||
required this.hosting,
|
required this.hosting,
|
||||||
required this.child
|
required this.child
|
||||||
}): tokenError = false, launched = false;
|
}): tokenError = false, launched = false, movedToVirtualDesktop = false;
|
||||||
|
|
||||||
void kill() {
|
void kill() {
|
||||||
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
||||||
@@ -42,4 +43,4 @@ class GameInstance {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:ini/ini.dart';
|
import 'package:ini/ini.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_common/src/extension/types.dart';
|
||||||
import 'package:shelf/shelf_io.dart';
|
import 'package:shelf/shelf_io.dart';
|
||||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||||
import 'package:sync/semaphore.dart';
|
import 'package:sync/semaphore.dart';
|
||||||
@@ -33,8 +35,8 @@ Future<bool> freeBackendPort() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
||||||
var hostName = _getHostName(host);
|
var hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||||
var declaredScheme = _getScheme(host);
|
var declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||||
try{
|
try{
|
||||||
var uri = Uri(
|
var uri = Uri(
|
||||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||||
@@ -52,10 +54,6 @@ Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
|
|
||||||
|
|
||||||
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
|
||||||
|
|
||||||
Stream<String?> watchMatchmakingIp() async* {
|
Stream<String?> watchMatchmakingIp() async* {
|
||||||
if(!matchmakerConfigFile.existsSync()){
|
if(!matchmakerConfigFile.existsSync()){
|
||||||
return;
|
return;
|
||||||
@@ -110,56 +108,4 @@ Future<void> writeMatchmakingIp(String text) async {
|
|||||||
config.set("GameServer", "ip", ip);
|
config.set("GameServer", "ip", ip);
|
||||||
config.set("GameServer", "port", port);
|
config.set("GameServer", "port", port);
|
||||||
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
|
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isMatchmakerPortFree() async => await pingMatchmaker(kDefaultMatchmakerHost, kDefaultMatchmakerPort) == null;
|
|
||||||
|
|
||||||
Future<bool> freeMatchmakerPort() async {
|
|
||||||
await killProcessByPort(kDefaultMatchmakerPort);
|
|
||||||
final standardResult = await isMatchmakerPortFree();
|
|
||||||
if(standardResult) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uri?> pingMatchmaker(String host, int port, [bool wss=false]) async {
|
|
||||||
var hostName = _getHostName(host);
|
|
||||||
var declaredScheme = _getScheme(host);
|
|
||||||
try{
|
|
||||||
var uri = Uri(
|
|
||||||
scheme: declaredScheme ?? (wss ? "wss" : "ws"),
|
|
||||||
host: hostName,
|
|
||||||
port: port
|
|
||||||
);
|
|
||||||
var completer = Completer<bool>();
|
|
||||||
var socket = await WebSocket.connect(uri.toString());
|
|
||||||
socket.listen(
|
|
||||||
(data) {
|
|
||||||
if(!completer.isCompleted) {
|
|
||||||
completer.complete(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) {
|
|
||||||
if(!completer.isCompleted) {
|
|
||||||
completer.complete(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
if(!completer.isCompleted) {
|
|
||||||
completer.complete(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
var result = await completer.future;
|
|
||||||
await socket.close();
|
|
||||||
return result ? uri : null;
|
|
||||||
}catch(_){
|
|
||||||
return wss || declaredScheme != null || isLocalHost(host) ? null : await pingMatchmaker(host, port, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _getHostName(String host) => host.replaceFirst("ws://", "").replaceFirst("wss://", "");
|
|
||||||
|
|
||||||
String? _getScheme(String host) => host.startsWith("ws://") ? "ws" : host.startsWith("wss://") ? "wss" : null;
|
|
||||||
@@ -8,6 +8,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_common/src/extension/types.dart';
|
||||||
|
|
||||||
const String kStopBuildDownloadSignal = "kill";
|
const String kStopBuildDownloadSignal = "kill";
|
||||||
|
|
||||||
@@ -23,61 +24,14 @@ Dio _buildDioInstance() {
|
|||||||
return dio;
|
return dio;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
|
final String _archiveSourceUrl = "http://185.203.216.3/versions.json";
|
||||||
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
||||||
const String _manifestSourceUrl = "http://manifest.simplyblk.xyz";
|
|
||||||
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
|
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
|
||||||
const String _unavailableError = "The build downloader is not available right now";
|
const String _unavailableError = "The build downloader is not available right now";
|
||||||
const String _genericError = "The build downloader is not working correctly";
|
const String _genericError = "The build downloader is not working correctly";
|
||||||
const int _maxErrors = 100;
|
const int _maxErrors = 100;
|
||||||
|
|
||||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||||
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () =>
|
|
||||||
HttpClient()
|
|
||||||
..badCertificateCallback =
|
|
||||||
(X509Certificate cert, String host, int port) => true;
|
|
||||||
|
|
||||||
final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]);
|
|
||||||
final data = <FortniteBuild>[];
|
|
||||||
for(final result in results) {
|
|
||||||
data.addAll(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<FortniteBuild>> _fetchManifestBuilds() async {
|
|
||||||
try {
|
|
||||||
final response = await _dio.get<String>(
|
|
||||||
"$_manifestSourceUrl/versions.json",
|
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"Accept-Encoding": "*",
|
|
||||||
"Cookie": "_c_t_c=1"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
final body = response.data;
|
|
||||||
return jsonDecode(body!).map((version) {
|
|
||||||
final nameParts = version.split("-");
|
|
||||||
if(nameParts.length < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final name = nameParts[1];
|
|
||||||
return FortniteBuild(
|
|
||||||
identifier: name,
|
|
||||||
version: "Fortnite ${name}",
|
|
||||||
link: "$_manifestSourceUrl/$name/$name.manifest",
|
|
||||||
source: FortniteBuildSource.manifest
|
|
||||||
);
|
|
||||||
}).whereType<FortniteBuild>().toList();
|
|
||||||
}catch(_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
|
|
||||||
final response = await _dio.get<String>(
|
final response = await _dio.get<String>(
|
||||||
_archiveSourceUrl,
|
_archiveSourceUrl,
|
||||||
options: Options(
|
options: Options(
|
||||||
@@ -88,32 +42,15 @@ Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(response.data ?? "{}");
|
||||||
var results = <FortniteBuild>[];
|
var results = <FortniteBuild>[];
|
||||||
for (var line in response.data?.split("\n") ?? []) {
|
for(final entry in data.entries) {
|
||||||
if(!line.startsWith("|")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = line.substring(1, line.length - 1).split("|");
|
|
||||||
if(parts.isEmpty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var link = parts.last.trim();
|
|
||||||
if(!link.endsWith(".zip") && !link.endsWith(".rar")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var version = parts.first.trim();
|
|
||||||
version = version.substring(0, version.indexOf("-"));
|
|
||||||
results.add(FortniteBuild(
|
results.add(FortniteBuild(
|
||||||
identifier: version,
|
identifier: entry.key,
|
||||||
version: "Fortnite $version",
|
version: "${entry.value["title"]} (${entry.key})",
|
||||||
link: link,
|
link: entry.value["url"]
|
||||||
source: FortniteBuildSource.archive
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,107 +58,23 @@ Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
|
|||||||
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
||||||
try {
|
try {
|
||||||
final stopped = _setupLifecycle(options);
|
final stopped = _setupLifecycle(options);
|
||||||
switch(options.build.source) {
|
final outputDir = Directory("${options.destination.path}\\.build");
|
||||||
case FortniteBuildSource.archive:
|
await outputDir.create(recursive: true);
|
||||||
final outputDir = Directory("${options.destination.path}\\.build");
|
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
||||||
await outputDir.create(recursive: true);
|
final extension = path.extension(fileName);
|
||||||
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
|
final tempFile = File("${outputDir.path}\\$fileName");
|
||||||
final extension = path.extension(fileName);
|
if(await tempFile.exists()) {
|
||||||
final tempFile = File("${outputDir.path}\\$fileName");
|
await tempFile.delete(recursive: true);
|
||||||
if(await tempFile.exists()) {
|
|
||||||
await tempFile.delete(recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
final response = _downloadArchive(options, tempFile, startTime);
|
|
||||||
await Future.any([stopped.future, response]);
|
|
||||||
if(!stopped.isCompleted) {
|
|
||||||
await _extractArchive(stopped, extension, tempFile, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(outputDir);
|
|
||||||
break;
|
|
||||||
case FortniteBuildSource.manifest:
|
|
||||||
final response = await _dio.get<String>(
|
|
||||||
options.build.link,
|
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"Cookie": "_c_t_c=1"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
final manifest = FortniteBuildManifestFile.fromJson(jsonDecode(response.data!));
|
|
||||||
|
|
||||||
final totalBytes = manifest.size;
|
|
||||||
final outputDir = options.destination;
|
|
||||||
await outputDir.create(recursive: true);
|
|
||||||
|
|
||||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
final codec = GZipCodec();
|
|
||||||
var completedBytes = 0;
|
|
||||||
var lastPercentage = 0.0;
|
|
||||||
|
|
||||||
final writers = manifest.chunks.map((chunkedFile) async {
|
|
||||||
final outputFile = File('${outputDir.path}/${chunkedFile.file}');
|
|
||||||
if(outputFile.existsSync()) {
|
|
||||||
if(outputFile.lengthSync() != chunkedFile.fileSize) {
|
|
||||||
await outputFile.delete();
|
|
||||||
} else {
|
|
||||||
completedBytes += chunkedFile.fileSize;
|
|
||||||
final percentage = completedBytes * 100 / totalBytes;
|
|
||||||
if(percentage - lastPercentage > 0.1) {
|
|
||||||
_onProgress(
|
|
||||||
startTime,
|
|
||||||
DateTime.now().millisecondsSinceEpoch,
|
|
||||||
percentage,
|
|
||||||
false,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await outputFile.parent.create(recursive: true);
|
|
||||||
for(final chunkId in chunkedFile.chunksIds) {
|
|
||||||
final response = await _dio.get<Uint8List>(
|
|
||||||
"$_manifestSourceUrl/${options.build.identifier}/$chunkId.chunk",
|
|
||||||
options: Options(
|
|
||||||
responseType: ResponseType.bytes,
|
|
||||||
headers: {
|
|
||||||
"Accept-Encoding": "gzip",
|
|
||||||
"Cookie": "_c_t_c=1"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
|
||||||
var responseBody = response.data;
|
|
||||||
if(responseBody == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final decodedBody = codec.decode(responseBody);
|
|
||||||
await outputFile.writeAsBytes(
|
|
||||||
decodedBody,
|
|
||||||
mode: FileMode.append,
|
|
||||||
flush: true
|
|
||||||
);
|
|
||||||
completedBytes += decodedBody.length;
|
|
||||||
|
|
||||||
final percentage = completedBytes * 100 / totalBytes;
|
|
||||||
if(percentage - lastPercentage > 0.1) {
|
|
||||||
_onProgress(
|
|
||||||
startTime,
|
|
||||||
DateTime.now().millisecondsSinceEpoch,
|
|
||||||
percentage,
|
|
||||||
false,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await Future.any([stopped.future, Future.wait(writers)]);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final response = _downloadArchive(options, tempFile, startTime);
|
||||||
|
await Future.any([stopped.future, response]);
|
||||||
|
if(!stopped.isCompleted) {
|
||||||
|
await _extractArchive(stopped, extension, tempFile, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(outputDir);
|
||||||
}catch(error) {
|
}catch(error) {
|
||||||
_onError(error, options);
|
_onError(error, options);
|
||||||
}
|
}
|
||||||
@@ -358,7 +211,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
|||||||
throw ArgumentError("Unexpected file extension: $extension}");
|
throw ArgumentError("Unexpected file extension: $extension}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await Future.any([stopped.future, watchProcess(process.pid)]);
|
await Future.any([stopped.future, process.exitCode]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
|
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
|
||||||
@@ -385,7 +238,6 @@ void _onError(Object? error, FortniteBuildDownloadOptions options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
||||||
var stopped = Completer();
|
var stopped = Completer();
|
||||||
var lifecyclePort = ReceivePort();
|
var lifecyclePort = ReceivePort();
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
|||||||
throw Exception("Cannot mutate length of binary file");
|
throw Exception("Cannot mutate length of binary file");
|
||||||
}
|
}
|
||||||
|
|
||||||
var read = await file.readAsBytes();
|
final read = await file.readAsBytes();
|
||||||
var length = await file.length();
|
final length = await file.length();
|
||||||
var readOffset = 0;
|
var readOffset = 0;
|
||||||
var patchOffset = -1;
|
var patchOffset = -1;
|
||||||
var patchCount = 0;
|
var patchCount = 0;
|
||||||
@@ -50,7 +50,6 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
|||||||
readOffset++;
|
readOffset++;
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Offset: $patchOffset");
|
|
||||||
if(patchOffset == -1) {
|
if(patchOffset == -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,41 +39,4 @@ Future<bool> delete(FileSystemEntity file) async {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension FortniteVersionExtension on FortniteVersion {
|
|
||||||
static File? findExecutable(Directory directory, String name) {
|
|
||||||
try{
|
|
||||||
var result = directory.listSync(recursive: true)
|
|
||||||
.firstWhere((element) => path.basename(element.path) == name);
|
|
||||||
return File(result.path);
|
|
||||||
}catch(_){
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
File? get gameExecutable => findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
|
||||||
|
|
||||||
Future<File?> get headlessGameExecutable async {
|
|
||||||
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Headless.exe");
|
|
||||||
if(result != null) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
|
||||||
if(original == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Headless.exe");
|
|
||||||
await original.copy(output.path);
|
|
||||||
await Isolate.run(() => patchHeadless(output));
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
|
|
||||||
|
|
||||||
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
|
||||||
|
|
||||||
File? get splashBitmap => findExecutable(location, "Splash.bmp");
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import 'package:path/path.dart' as path;
|
|||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_common/src/extension/process.dart';
|
||||||
import 'package:sync/semaphore.dart';
|
import 'package:sync/semaphore.dart';
|
||||||
import 'package:win32/win32.dart';
|
import 'package:win32/win32.dart';
|
||||||
|
|
||||||
@@ -282,10 +283,4 @@ class _ExtendedProcess extends Process {
|
|||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension ProcessExtension on Process {
|
|
||||||
Stream<String> get stdOutput => this.stdout.expand((event) => utf8.decode(event).split("\n"));
|
|
||||||
|
|
||||||
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event).split("\n"));
|
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 774 B After Width: | Height: | Size: 522 B |
Binary file not shown.
|
Before Width: | Height: | Size: 522 B |
4
gui/assets/info/en/1. What is Project Reboot
Normal file
4
gui/assets/info/en/1. What is Project Reboot
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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."
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Your version of Fortnite is corrupted, download it again from the launcher, or use another build.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Support for LawinV2 is available in the launcher.
|
||||||
|
To use the backend, select a local or remote backend from the "Backend" tab in the launcher.
|
||||||
|
To use the credentials, click on the avatar on the top left of the launcher and enter your email and password
|
||||||
7
gui/assets/info/en/2. What is a Fortnite game server
Normal file
7
gui/assets/info/en/2. What is a Fortnite game server
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
If you have ever played Minecraft multiplayer, you might know that the servers you join are hosted on a computer running a program, called Minecraft Game Server.
|
||||||
|
While the Minecraft Game server is written by the creators of Minecraft, Mojang, Epic Games doesn't provide an equivalent for Fortnite.
|
||||||
|
By exploiting the Fortnite internals, though, it's possible to create a game server just like in Minecraft: this is in easy terms what Project Reboot does.
|
||||||
|
Some Fortnite versions support running this game server in the background without rendering the game("headless"), while others still require the full game to be open.
|
||||||
|
Just like in Minecraft, you need a game client to play the game and one to host the server.
|
||||||
|
By default, a game server is automatically started on your PC when you start a Fortnite version from the "Play" section in the launcher.
|
||||||
|
If you want to play in another way, for example by joining a server hosted by one of your friends instead of running one yourself, you can checkout the "Multiplayer" section in the "Play" tab of the launcher.
|
||||||
4
gui/assets/info/en/3. Types of Fortnite game server
Normal file
4
gui/assets/info/en/3. Types of Fortnite game server
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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.
|
||||||
22
gui/assets/info/en/4. How can others join my game server
Normal file
22
gui/assets/info/en/4. How can others join my game server
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
For others to join your game server, port 7777 must be accessible on your PC.
|
||||||
|
One option is to use a private VPN service like Hamachi or Radmin, but all of the players will need to download this software.
|
||||||
|
The best solution is to use port forwarding:
|
||||||
|
1. Set a static IP
|
||||||
|
If you don't have already a static IP set, set one by following any tutorial on Google
|
||||||
|
2. Log into your router's admin panel
|
||||||
|
Usually this can be accessed on any web browser by going to http://192.168.1.1/
|
||||||
|
You might need a username and a password to log in: refer to your router's manual for precise instructions
|
||||||
|
3. Find the port forwarding section
|
||||||
|
Once logged in into the admin panel, navigate to the port forwarding section of your router's settings
|
||||||
|
This location may vary from router to router, but it's typically labelled as "Port Forwarding," "Port Mapping" or "Virtual Server"
|
||||||
|
Refer to your router's manual for precise instructions
|
||||||
|
4. Add a port forwarding rule
|
||||||
|
Now, you'll need to create a new port forwarding rule. Here's what you'll typically need to specify:
|
||||||
|
- Service Name: Choose a name for your port forwarding rule (e.g., "Fortnite Game Server")
|
||||||
|
- Port Number: Enter 7777 for both the external and internal ports
|
||||||
|
- Protocol: Select the UDP protocol
|
||||||
|
- Internal IP Address: Enter the static IP address you set earlier
|
||||||
|
- Enable: Make sure the port forwarding rule is enabled
|
||||||
|
5. Save and apply the changes
|
||||||
|
After configuring the port forwarding rule, save your changes and apply them
|
||||||
|
This step may involve clicking a "Save" or "Apply" button on your router's web interface
|
||||||
6
gui/assets/info/en/5. What is a backend
Normal file
6
gui/assets/info/en/5. What is a backend
Normal file
@@ -0,0 +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."
|
||||||
|
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.
|
||||||
4
gui/assets/info/en/6. What is the Unreal Engine console
Normal file
4
gui/assets/info/en/6. What is the Unreal Engine console
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Many Fortnite versions don't support entering in game by clicking the \"Play\" button.
|
||||||
|
Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1
|
||||||
|
Keep in mind that the Unreal Engine console key is controlled by the backend, so this is true only if you are using the embedded backend: custom backends might use different keys.
|
||||||
|
When using the embedded backend, you can customize the key used to open the console in the \"Backend\" tab of the Reboot Launcher.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
To resolve this issue:
|
||||||
|
- Check that your backend is working correctly from the "Backend" tab
|
||||||
|
- If you are using a custom backend, try to use the embedded one
|
||||||
|
- Try to run the backend as detached by enabling the "Detached" option in the "Backend" tab
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
As explained in the "What is a Fortnite game server?" section, one instance of Fortnite is used to host the game server, while the other is used to let you play.
|
||||||
|
The Fortnite instance used up by the game server is usually frozen, so it should be hard to use the wrong one to try to play.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
As explained in the "What is the Unreal Engine console?" section, the "Play" button doesn't work in many Fortnite versions.
|
||||||
|
Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1
|
||||||
@@ -35,6 +35,8 @@
|
|||||||
"hostHeadlessDescription": "Runs Fortnite without graphics to optimize resources usage, may not work for old seasons",
|
"hostHeadlessDescription": "Runs Fortnite without graphics to optimize resources usage, may not work for old seasons",
|
||||||
"hostVirtualDesktopName": "Virtual desktop",
|
"hostVirtualDesktopName": "Virtual desktop",
|
||||||
"hostVirtualDesktopDescription": "Runs Fortnite in a virtual desktop if headless is not supported",
|
"hostVirtualDesktopDescription": "Runs Fortnite in a virtual desktop if headless is not supported",
|
||||||
|
"hostAutomaticRestartName": "Automatic restart",
|
||||||
|
"hostAutomaticRestartDescription": "Automatically restarts your game server when the match ends",
|
||||||
"hostShareName": "Share",
|
"hostShareName": "Share",
|
||||||
"hostShareDescription": "Make it easy for other people to join your server with the options in this section",
|
"hostShareDescription": "Make it easy for other people to join your server with the options in this section",
|
||||||
"hostShareLinkName": "Link",
|
"hostShareLinkName": "Link",
|
||||||
@@ -59,26 +61,8 @@
|
|||||||
"findServer": "Find a server",
|
"findServer": "Find a server",
|
||||||
"copyIp": "Copy IP",
|
"copyIp": "Copy IP",
|
||||||
"hostName": "Host",
|
"hostName": "Host",
|
||||||
"matchmakerName": "Matchmaker",
|
|
||||||
"matchmakerTypeName": "Type",
|
|
||||||
"matchmakerTypeDescription": "The type of matchmaker to use when queueing for a game",
|
|
||||||
"matchmakerConfigurationHostName": "Host",
|
|
||||||
"matchmakerConfigurationHostDescription": "The hostname of the matchmaker",
|
|
||||||
"matchmakerConfigurationPortName": "Port",
|
|
||||||
"matchmakerConfigurationPortDescription": "The port of the matchmaker",
|
|
||||||
"matchmakerConfigurationAddressName": "Game server address",
|
"matchmakerConfigurationAddressName": "Game server address",
|
||||||
"matchmakerConfigurationAddressDescription": "The address of the game server used by the matchmaker",
|
"matchmakerConfigurationAddressDescription": "The address of the game server used by the backend",
|
||||||
"matchmakerConfigurationDetachedName": "Detached",
|
|
||||||
"matchmakerConfigurationDetachedDescription": "Whether a separate process should be spawned, useful for debugging",
|
|
||||||
"matchmakerInstallationDirectoryName": "Installation directory",
|
|
||||||
"matchmakerInstallationDirectoryDescription": "Opens the folder where the embedded matchmaker is located",
|
|
||||||
"matchmakerInstallationDirectoryContent": "Show files",
|
|
||||||
"matchmakerResetDefaultsName": "Reset",
|
|
||||||
"matchmakerResetDefaultsDescription": "Resets the matchmaker's settings to their default values",
|
|
||||||
"matchmakerResetDefaultsContent": "Reset",
|
|
||||||
"matchmakerResetDefaultsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
|
|
||||||
"matchmakerResetDefaultsDialogSecondaryAction": "Close",
|
|
||||||
"matchmakerResetDefaultsDialogPrimaryAction": "Reset",
|
|
||||||
"playName": "Play",
|
"playName": "Play",
|
||||||
"playGameServerName": "Multiplayer",
|
"playGameServerName": "Multiplayer",
|
||||||
"playGameServerDescription": "See all the available options to start playing",
|
"playGameServerDescription": "See all the available options to start playing",
|
||||||
@@ -95,8 +79,10 @@
|
|||||||
"playGameServerCustomDescription": "Join a game server using its public IP address",
|
"playGameServerCustomDescription": "Join a game server using its public IP address",
|
||||||
"playGameServerCustomContent": "Enter IP",
|
"playGameServerCustomContent": "Enter IP",
|
||||||
"settingsName": "Settings",
|
"settingsName": "Settings",
|
||||||
"settingsClientName": "Configuration",
|
"settingsClientName": "Internal files",
|
||||||
"settingsClientDescription": "Configure the internals of Fortnite",
|
"settingsClientDescription": "Configure the internal files used by the launcher for Fortnite",
|
||||||
|
"settingsClientOptionsName": "Options",
|
||||||
|
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
|
||||||
"settingsClientConsoleName": "Unreal engine console",
|
"settingsClientConsoleName": "Unreal engine console",
|
||||||
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
|
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
|
||||||
"settingsClientConsoleKeyName": "Unreal engine console key",
|
"settingsClientConsoleKeyName": "Unreal engine console key",
|
||||||
@@ -108,17 +94,23 @@
|
|||||||
"settingsClientArgsName": "Custom launch arguments",
|
"settingsClientArgsName": "Custom launch arguments",
|
||||||
"settingsClientArgsDescription": "Additional arguments to use when launching the game",
|
"settingsClientArgsDescription": "Additional arguments to use when launching the game",
|
||||||
"settingsClientArgsPlaceholder": "Arguments...",
|
"settingsClientArgsPlaceholder": "Arguments...",
|
||||||
"settingsServerName": "Configuration",
|
"settingsServerName": "Internal files",
|
||||||
"settingsServerSubtitle": "Configure the internals of your game server",
|
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
|
||||||
|
"settingsServerOptionsName": "Options",
|
||||||
|
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
|
||||||
|
"settingsServerTypeName": "Type",
|
||||||
|
"settingsServerTypeDescription": "The type of game server to inject",
|
||||||
|
"settingsServerTypeEmbeddedName": "Embedded",
|
||||||
|
"settingsServerTypeCustomName": "Custom",
|
||||||
"settingsServerFileName": "Implementation",
|
"settingsServerFileName": "Implementation",
|
||||||
"settingsServerFileDescription": "Creates a game server to host matches",
|
"settingsServerFileDescription": "The file injected to create the game server",
|
||||||
"settingsServerPortName": "Port",
|
"settingsServerPortName": "Port",
|
||||||
"settingsServerPortDescription": "The port used by the game server dll",
|
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
|
||||||
"settingsServerMirrorName": "Update mirror",
|
"settingsServerMirrorName": "Update mirror",
|
||||||
"settingsServerMirrorDescription": "The URL used to update the game server dll",
|
"settingsServerMirrorDescription": "The URL used to update the game server dll",
|
||||||
"settingsServerMirrorPlaceholder": "mirror",
|
"settingsServerMirrorPlaceholder": "mirror",
|
||||||
"settingsServerTimerName": "Update timer",
|
"settingsServerTimerName": "Update timer",
|
||||||
"settingsServerTimerSubtitle": "Determines when the game server dll should be updated",
|
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
|
||||||
"settingsUtilsName": "Launcher",
|
"settingsUtilsName": "Launcher",
|
||||||
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
|
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
|
||||||
"settingsUtilsInstallationDirectoryName": "Installation directory",
|
"settingsUtilsInstallationDirectoryName": "Installation directory",
|
||||||
@@ -171,23 +163,23 @@
|
|||||||
"passwordPlaceholder": "Type your password, if you want to use one",
|
"passwordPlaceholder": "Type your password, if you want to use one",
|
||||||
"cancelProfileChanges": "Cancel",
|
"cancelProfileChanges": "Cancel",
|
||||||
"saveProfileChanges": "Save",
|
"saveProfileChanges": "Save",
|
||||||
"startingServer": "Starting the {name}...",
|
"startingServer": "Starting the backend...",
|
||||||
"startedServer": "The {name} was started successfully",
|
"startedServer": "The backend was started successfully",
|
||||||
"checkedServer": "The {name} works correctly",
|
"checkedServer": "The backend works correctly",
|
||||||
"startServerError": "An error occurred while starting the {name}: {error}",
|
"startServerError": "An error occurred while starting the backend: {error}",
|
||||||
"localServerError": "The local {name} doesn't work correctly: {error}",
|
"localServerError": "The local backend doesn't work correctly: {error}",
|
||||||
"stoppingServer": "Stopping the {name}...",
|
"stoppingServer": "Stopping the backend...",
|
||||||
"stoppedServer": "The {name} was stopped successfully",
|
"stoppedServer": "The backend was stopped successfully",
|
||||||
"stopServerError": "An error occurred while stopping the {name}: {error}",
|
"stopServerError": "An error occurred while stopping the backend: {error}",
|
||||||
"missingHostNameError": "Missing hostname in the {name} configuration",
|
"missingHostNameError": "Missing hostname in the {name} configuration",
|
||||||
"missingPortError": "Missing port in the {name} configuration",
|
"missingPortError": "Missing port in the backend configuration",
|
||||||
"illegalPortError": "Invalid port in the {name} configuration",
|
"illegalPortError": "Invalid port in the backend configuration",
|
||||||
"freeingPort": "Freeing port {port}...",
|
"freeingPort": "Freeing the backend port...",
|
||||||
"freedPort": "Port {port} was freed successfully",
|
"freedPort": "The backend port was freed successfully",
|
||||||
"freePortError": "An error occurred while freeing port {port}: {error}",
|
"freePortError": "An error occurred while freeing the backend port: {error}",
|
||||||
"pingingRemoteServer": "Pinging the remote {name}...",
|
"pingingRemoteServer": "Pinging the remote backend...",
|
||||||
"pingingLocalServer": "Pinging the {type} {name}...",
|
"pingingLocalServer": "Pinging the {type} backend...",
|
||||||
"pingError": "Cannot ping the {type} {name}",
|
"pingError": "Cannot ping the {type} backend",
|
||||||
"joinSelfServer": "You can't join your own server",
|
"joinSelfServer": "You can't join your own server",
|
||||||
"wrongServerPassword": "Wrong password: please try again",
|
"wrongServerPassword": "Wrong password: please try again",
|
||||||
"offlineServer": "This server isn't online right now: please try again later",
|
"offlineServer": "This server isn't online right now: please try again later",
|
||||||
@@ -236,9 +228,9 @@
|
|||||||
"embedded": "Embedded",
|
"embedded": "Embedded",
|
||||||
"remote": "Remote",
|
"remote": "Remote",
|
||||||
"local": "Local",
|
"local": "Local",
|
||||||
"checkServer": "Check {name}",
|
"checkServer": "Check backend",
|
||||||
"startServer": "Start {name}",
|
"startServer": "Start backend",
|
||||||
"stopServer": "Stop {name}",
|
"stopServer": "Stop backend",
|
||||||
"startHosting": "Start hosting",
|
"startHosting": "Start hosting",
|
||||||
"stopHosting": "Stop hosting",
|
"stopHosting": "Stop hosting",
|
||||||
"startGame": "Start fortnite",
|
"startGame": "Start fortnite",
|
||||||
@@ -301,5 +293,10 @@
|
|||||||
"settingsLogsSelectFolder": "Select a folder",
|
"settingsLogsSelectFolder": "Select a folder",
|
||||||
"settingsLogsCreating": "Creating logs archive...",
|
"settingsLogsCreating": "Creating logs archive...",
|
||||||
"settingsLogsCreated": "Logs archive created successfully",
|
"settingsLogsCreated": "Logs archive created successfully",
|
||||||
"settingsLogsCreatedShowFile": "Show file"
|
"settingsLogsCreatedShowFile": "Show file",
|
||||||
|
"updateAvailable": "Version {version} is now available",
|
||||||
|
"updateAvailableAction": "Download",
|
||||||
|
"gameServerEnd": "The match ended",
|
||||||
|
"gameServerRestart": "The server will restart in {timeInSeconds} seconds",
|
||||||
|
"gameServerShutdown": "The server will shutdown in {timeInSeconds} seconds"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,31 +10,36 @@ import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
|||||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/build_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/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/info_controller.dart';
|
import 'package:reboot_launcher/src/controller/info_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/update_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/abstract/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/implementation/error.dart';
|
import 'package:reboot_launcher/src/dialog/implementation/error.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/implementation/server.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/page/implementation/home_page.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/info.dart';
|
||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
import 'package:url_protocol/url_protocol.dart';
|
import 'package:url_protocol/url_protocol.dart';
|
||||||
|
import 'package:version/version.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
const double kDefaultWindowWidth = 1536;
|
const double kDefaultWindowWidth = 1536;
|
||||||
const double kDefaultWindowHeight = 1024;
|
const double kDefaultWindowHeight = 1024;
|
||||||
const String kCustomUrlSchema = "Reboot";
|
const String kCustomUrlSchema = "Reboot";
|
||||||
|
|
||||||
|
Version? appVersion;
|
||||||
|
|
||||||
class _MyHttpOverrides extends HttpOverrides {
|
class _MyHttpOverrides extends HttpOverrides {
|
||||||
@override
|
@override
|
||||||
HttpClient createHttpClient(SecurityContext? context){
|
HttpClient createHttpClient(SecurityContext? context){
|
||||||
@@ -53,9 +58,19 @@ void main() => runZonedGuarded(
|
|||||||
url: supabaseUrl,
|
url: supabaseUrl,
|
||||||
anonKey: supabaseAnonKey
|
anonKey: supabaseAnonKey
|
||||||
);
|
);
|
||||||
|
await localNotifier.setup(
|
||||||
|
appName: 'Reboot Launcher',
|
||||||
|
shortcutPolicy: ShortcutPolicy.ignore
|
||||||
|
);
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await SystemTheme.accentColor.load();
|
await SystemTheme.accentColor.load();
|
||||||
_initWindow();
|
_initWindow();
|
||||||
|
initInfoTiles();
|
||||||
|
final versionError = await _initVersion();
|
||||||
|
if(versionError != null) {
|
||||||
|
errors.add(versionError);
|
||||||
|
}
|
||||||
|
|
||||||
final storageError = await _initStorage();
|
final storageError = await _initStorage();
|
||||||
if(storageError != null) {
|
if(storageError != null) {
|
||||||
errors.add(storageError);
|
errors.add(storageError);
|
||||||
@@ -84,10 +99,20 @@ void _handleErrors(List<Object?> errors) {
|
|||||||
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
|
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Object?> _initVersion() async {
|
||||||
|
try {
|
||||||
|
final packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
appVersion = Version.parse(packageInfo.version);
|
||||||
|
return null;
|
||||||
|
}catch(error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _checkGameServer() async {
|
Future<void> _checkGameServer() async {
|
||||||
try {
|
try {
|
||||||
var matchmakerController = Get.find<MatchmakerController>();
|
var backendController = Get.find<BackendController>();
|
||||||
var address = matchmakerController.gameServerAddress.text;
|
var address = backendController.gameServerAddress.text;
|
||||||
if(isLocalHost(address)) {
|
if(isLocalHost(address)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -97,8 +122,8 @@ Future<void> _checkGameServer() async {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldOwner = matchmakerController.gameServerOwner.value;
|
var oldOwner = backendController.gameServerOwner.value;
|
||||||
matchmakerController.joinLocalHost();
|
backendController.joinLocalHost();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||||
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||||
severity: InfoBarSeverity.warning,
|
severity: InfoBarSeverity.warning,
|
||||||
@@ -128,11 +153,11 @@ Future<Object?> _initUrlHandler() async {
|
|||||||
|
|
||||||
void _joinServer(Uri uri) {
|
void _joinServer(Uri uri) {
|
||||||
var hostingController = Get.find<HostingController>();
|
var hostingController = Get.find<HostingController>();
|
||||||
var matchmakerController = Get.find<MatchmakerController>();
|
var backendController = Get.find<BackendController>();
|
||||||
var uuid = _parseCustomUrl(uri);
|
var uuid = _parseCustomUrl(uri);
|
||||||
var server = hostingController.findServerById(uuid);
|
var server = hostingController.findServerById(uuid);
|
||||||
if(server != null) {
|
if(server != null) {
|
||||||
matchmakerController.joinServer(hostingController.uuid, server);
|
backendController.joinServer(hostingController.uuid, server);
|
||||||
}else {
|
}else {
|
||||||
showInfoBar(
|
showInfoBar(
|
||||||
translations.noServerFound,
|
translations.noServerFound,
|
||||||
@@ -176,13 +201,11 @@ Future<Object?> _initStorage() async {
|
|||||||
try {
|
try {
|
||||||
await GetStorage("game", settingsDirectory.path).initStorage;
|
await GetStorage("game", settingsDirectory.path).initStorage;
|
||||||
await GetStorage("backend", settingsDirectory.path).initStorage;
|
await GetStorage("backend", settingsDirectory.path).initStorage;
|
||||||
await GetStorage("matchmaker", settingsDirectory.path).initStorage;
|
|
||||||
await GetStorage("update", settingsDirectory.path).initStorage;
|
await GetStorage("update", settingsDirectory.path).initStorage;
|
||||||
await GetStorage("settings", settingsDirectory.path).initStorage;
|
await GetStorage("settings", settingsDirectory.path).initStorage;
|
||||||
await GetStorage("hosting", settingsDirectory.path).initStorage;
|
await GetStorage("hosting", settingsDirectory.path).initStorage;
|
||||||
Get.put(GameController());
|
Get.put(GameController());
|
||||||
Get.put(BackendController());
|
Get.put(BackendController());
|
||||||
Get.put(MatchmakerController());
|
|
||||||
Get.put(BuildController());
|
Get.put(BuildController());
|
||||||
Get.put(SettingsController());
|
Get.put(SettingsController());
|
||||||
Get.put(HostingController());
|
Get.put(HostingController());
|
||||||
|
|||||||
@@ -1,43 +1,235 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
|
|
||||||
class BackendController extends ServerController {
|
class BackendController extends GetxController {
|
||||||
late RxBool detached;
|
late final GetStorage storage;
|
||||||
|
late final TextEditingController host;
|
||||||
|
late final TextEditingController port;
|
||||||
|
late final Rx<ServerType> type;
|
||||||
|
late final TextEditingController gameServerAddress;
|
||||||
|
late final FocusNode gameServerAddressFocusNode;
|
||||||
|
late final RxnString gameServerOwner;
|
||||||
|
late final RxBool started;
|
||||||
|
late final RxBool detached;
|
||||||
|
StreamSubscription? worker;
|
||||||
|
HttpServer? localServer;
|
||||||
|
HttpServer? remoteServer;
|
||||||
|
|
||||||
BackendController() : super() {
|
BackendController() {
|
||||||
|
storage = GetStorage("backend");
|
||||||
|
started = RxBool(false);
|
||||||
|
type = Rx(ServerType.values.elementAt(storage.read("type") ?? 0));
|
||||||
|
type.listen((value) {
|
||||||
|
host.text = _readHost();
|
||||||
|
port.text = _readPort();
|
||||||
|
storage.write("type", value.index);
|
||||||
|
if (!started.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stop();
|
||||||
|
});
|
||||||
|
host = TextEditingController(text: _readHost());
|
||||||
|
host.addListener(() =>
|
||||||
|
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 = RxBool(storage.read("detached") ?? false);
|
||||||
detached.listen((value) => storage.write("detached", value));
|
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(() {
|
||||||
|
var newValue = gameServerAddress.text;
|
||||||
|
if(newValue.trim().toLowerCase() == lastValue.trim().toLowerCase()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastValue = newValue;
|
||||||
|
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
|
||||||
|
storage.write("game_server_address", newValue);
|
||||||
|
writeMatchmakingIp(newValue);
|
||||||
|
});
|
||||||
|
watchMatchmakingIp().listen((event) {
|
||||||
|
if(event != null && gameServerAddress.text != event) {
|
||||||
|
gameServerAddress.text = event;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gameServerAddressFocusNode = FocusNode();
|
||||||
|
gameServerOwner = RxnString(storage.read("game_server_owner"));
|
||||||
|
gameServerOwner.listen((value) => storage.write("game_server_owner", value));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void reset() async {
|
||||||
String get controllerName => translations.backendName.toLowerCase();
|
type.value = ServerType.values.elementAt(0);
|
||||||
|
for (final type in ServerType.values) {
|
||||||
|
storage.write("${type.name}_host", null);
|
||||||
|
storage.write("${type.name}_port", null);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
|
||||||
String get storageName => "backend";
|
port.text = kDefaultBackendPort.toString();
|
||||||
|
detached.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
String _readHost() {
|
||||||
String get defaultHost => kDefaultBackendHost;
|
String? value = storage.read("${type.value.name}_host");
|
||||||
|
if (value != null && value.isNotEmpty) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
if (type.value != ServerType.remote) {
|
||||||
int get defaultPort => kDefaultBackendPort;
|
return kDefaultBackendHost;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
return "";
|
||||||
Future<bool> get isPortFree => isBackendPortFree();
|
}
|
||||||
|
|
||||||
@override
|
String _readPort() =>
|
||||||
Future<bool> freePort() => freeBackendPort();
|
storage.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
|
||||||
|
|
||||||
@override
|
Stream<ServerResult> start() async* {
|
||||||
RebootPageType get pageType => RebootPageType.backend;
|
try {
|
||||||
|
if(started.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
final hostData = this.host.text.trim();
|
||||||
Future<Process> startEmbeddedInternal() => startEmbeddedBackend(detached.value);
|
final portData = this.port.text.trim();
|
||||||
|
if(type() != ServerType.local) {
|
||||||
|
started.value = true;
|
||||||
|
yield ServerResult(ServerResultType.starting);
|
||||||
|
}else {
|
||||||
|
started.value = false;
|
||||||
|
if(portData != kDefaultBackendPort.toString()) {
|
||||||
|
yield ServerResult(ServerResultType.starting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
if (hostData.isEmpty) {
|
||||||
Future<Uri?> pingServer(String host, int port) => pingBackend(host, port);
|
yield ServerResult(ServerResultType.missingHostError);
|
||||||
|
started.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portData.isEmpty) {
|
||||||
|
yield ServerResult(ServerResultType.missingPortError);
|
||||||
|
started.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final portNumber = int.tryParse(portData);
|
||||||
|
if (portNumber == null) {
|
||||||
|
yield ServerResult(ServerResultType.illegalPortError);
|
||||||
|
started.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((type() != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
|
||||||
|
yield ServerResult(ServerResultType.freeingPort);
|
||||||
|
final result = await freeBackendPort();
|
||||||
|
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
||||||
|
if(!result) {
|
||||||
|
started.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(type()){
|
||||||
|
case ServerType.embedded:
|
||||||
|
final process = await startEmbeddedBackend(detached.value);
|
||||||
|
final processPid = process.pid;
|
||||||
|
watchProcess(processPid).then((value) {
|
||||||
|
if(started()) {
|
||||||
|
started.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
yield ServerResult(ServerResultType.pingingRemote);
|
||||||
|
final uriResult = await pingBackend(hostData, portNumber);
|
||||||
|
if(uriResult == null) {
|
||||||
|
yield ServerResult(ServerResultType.pingError);
|
||||||
|
started.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteServer = await startRemoteBackendProxy(uriResult);
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
if(portData != kDefaultBackendPort.toString()) {
|
||||||
|
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.pingingLocal);
|
||||||
|
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
|
||||||
|
if(uriResult == null) {
|
||||||
|
yield ServerResult(ServerResultType.pingError);
|
||||||
|
remoteServer?.close(force: true);
|
||||||
|
localServer?.close(force: true);
|
||||||
|
started.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.startSuccess);
|
||||||
|
}catch(error, stackTrace) {
|
||||||
|
yield ServerResult(
|
||||||
|
ServerResultType.startError,
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace
|
||||||
|
);
|
||||||
|
remoteServer?.close(force: true);
|
||||||
|
localServer?.close(force: true);
|
||||||
|
started.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<ServerResult> stop() async* {
|
||||||
|
if(!started.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield ServerResult(ServerResultType.stopping);
|
||||||
|
started.value = false;
|
||||||
|
try{
|
||||||
|
switch(type()){
|
||||||
|
case ServerType.embedded:
|
||||||
|
killProcessByPort(kDefaultBackendPort);
|
||||||
|
break;
|
||||||
|
case ServerType.remote:
|
||||||
|
await remoteServer?.close(force: true);
|
||||||
|
remoteServer = null;
|
||||||
|
break;
|
||||||
|
case ServerType.local:
|
||||||
|
await localServer?.close(force: true);
|
||||||
|
localServer = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
yield ServerResult(ServerResultType.stopSuccess);
|
||||||
|
}catch(error, stackTrace){
|
||||||
|
yield ServerResult(
|
||||||
|
ServerResultType.stopError,
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace
|
||||||
|
);
|
||||||
|
started.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<ServerResult> toggle() async* {
|
||||||
|
if(started()) {
|
||||||
|
yield* stop();
|
||||||
|
}else {
|
||||||
|
yield* start();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,8 @@ import 'package:reboot_common/common.dart';
|
|||||||
class BuildController extends GetxController {
|
class BuildController extends GetxController {
|
||||||
List<FortniteBuild>? _builds;
|
List<FortniteBuild>? _builds;
|
||||||
Rxn<FortniteBuild> _selectedBuild;
|
Rxn<FortniteBuild> _selectedBuild;
|
||||||
Rx<FortniteBuildSource> _selectedBuildSource;
|
|
||||||
|
|
||||||
BuildController() : _selectedBuild = Rxn(),
|
BuildController() : _selectedBuild = Rxn();
|
||||||
_selectedBuildSource = Rx(FortniteBuildSource.manifest);
|
|
||||||
|
|
||||||
List<FortniteBuild>? get builds => _builds;
|
List<FortniteBuild>? get builds => _builds;
|
||||||
|
|
||||||
@@ -15,26 +13,10 @@ class BuildController extends GetxController {
|
|||||||
|
|
||||||
set selectedBuild(FortniteBuild? value) {
|
set selectedBuild(FortniteBuild? value) {
|
||||||
_selectedBuild.value = value;
|
_selectedBuild.value = value;
|
||||||
if(value != null && value.source != value.source) {
|
|
||||||
_selectedBuildSource.value = value.source;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FortniteBuildSource get selectedBuildSource => _selectedBuildSource.value;
|
|
||||||
|
|
||||||
set selectedBuildSource(FortniteBuildSource value) {
|
|
||||||
_selectedBuildSource.value = value;
|
|
||||||
final selected = selectedBuild;
|
|
||||||
if(selected == null || selected.source != value) {
|
|
||||||
final selectable = builds?.firstWhereOrNull((element) => element.source == value);
|
|
||||||
_selectedBuild.value = selectable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
set builds(List<FortniteBuild>? builds) {
|
set builds(List<FortniteBuild>? builds) {
|
||||||
_builds = builds;
|
_builds = builds;
|
||||||
final selectable = builds?.firstWhereOrNull((element) => element.source == selectedBuildSource);
|
_selectedBuild.value = builds?.firstOrNull;
|
||||||
_selectedBuild.value = selectable;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class HostingController extends GetxController {
|
|||||||
late final RxBool discoverable;
|
late final RxBool discoverable;
|
||||||
late final RxBool headless;
|
late final RxBool headless;
|
||||||
late final RxBool virtualDesktop;
|
late final RxBool virtualDesktop;
|
||||||
|
late final RxBool autoRestart;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late final RxBool published;
|
late final RxBool published;
|
||||||
late final Rxn<GameInstance> instance;
|
late final Rxn<GameInstance> instance;
|
||||||
@@ -36,6 +37,8 @@ class HostingController extends GetxController {
|
|||||||
headless.listen((value) => _storage.write("headless", value));
|
headless.listen((value) => _storage.write("headless", value));
|
||||||
virtualDesktop = RxBool(_storage.read("virtual_desktop") ?? true);
|
virtualDesktop = RxBool(_storage.read("virtual_desktop") ?? true);
|
||||||
virtualDesktop.listen((value) => _storage.write("virtual_desktop", value));
|
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);
|
started = RxBool(false);
|
||||||
published = RxBool(false);
|
published = RxBool(false);
|
||||||
showPassword = RxBool(false);
|
showPassword = RxBool(false);
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
|
|
||||||
class MatchmakerController extends ServerController {
|
|
||||||
late final TextEditingController gameServerAddress;
|
|
||||||
late final FocusNode gameServerAddressFocusNode;
|
|
||||||
late final RxnString gameServerOwner;
|
|
||||||
|
|
||||||
MatchmakerController() : super() {
|
|
||||||
gameServerAddress = TextEditingController(text: storage.read("game_server_address") ?? kDefaultMatchmakerHost);
|
|
||||||
var lastValue = gameServerAddress.text;
|
|
||||||
writeMatchmakingIp(lastValue);
|
|
||||||
gameServerAddress.addListener(() {
|
|
||||||
var newValue = gameServerAddress.text;
|
|
||||||
if(newValue.trim().toLowerCase() == lastValue.trim().toLowerCase()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastValue = newValue;
|
|
||||||
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
|
|
||||||
storage.write("game_server_address", newValue);
|
|
||||||
writeMatchmakingIp(newValue);
|
|
||||||
});
|
|
||||||
watchMatchmakingIp().listen((event) {
|
|
||||||
if(event != null && gameServerAddress.text != event) {
|
|
||||||
gameServerAddress.text = event;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gameServerAddressFocusNode = FocusNode();
|
|
||||||
gameServerOwner = RxnString(storage.read("game_server_owner"));
|
|
||||||
gameServerOwner.listen((value) => storage.write("game_server_owner", value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get controllerName => translations.matchmakerName.toLowerCase();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get storageName => "matchmaker";
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get defaultHost => kDefaultMatchmakerHost;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get defaultPort => kDefaultMatchmakerPort;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> get isPortFree => isMatchmakerPortFree();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> freePort() => freeMatchmakerPort();
|
|
||||||
|
|
||||||
@override
|
|
||||||
RebootPageType get pageType => RebootPageType.matchmaker;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Process> startEmbeddedInternal() => startEmbeddedMatchmaker();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Uri?> pingServer(String host, int port) => pingMatchmaker(host, port);
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:get_storage/get_storage.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
|
||||||
import 'package:sync/semaphore.dart';
|
|
||||||
|
|
||||||
abstract class ServerController extends GetxController {
|
|
||||||
late final GetStorage storage;
|
|
||||||
late final TextEditingController host;
|
|
||||||
late final TextEditingController port;
|
|
||||||
late final Rx<ServerType> type;
|
|
||||||
late final Semaphore semaphore;
|
|
||||||
late RxBool started;
|
|
||||||
StreamSubscription? worker;
|
|
||||||
HttpServer? localServer;
|
|
||||||
HttpServer? remoteServer;
|
|
||||||
|
|
||||||
ServerController() {
|
|
||||||
storage = GetStorage(storageName);
|
|
||||||
started = RxBool(false);
|
|
||||||
type = Rx(ServerType.values.elementAt(storage.read("type") ?? 0));
|
|
||||||
type.listen((value) {
|
|
||||||
host.text = _readHost();
|
|
||||||
port.text = _readPort();
|
|
||||||
storage.write("type", value.index);
|
|
||||||
if (!started.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop();
|
|
||||||
});
|
|
||||||
host = TextEditingController(text: _readHost());
|
|
||||||
host.addListener(() =>
|
|
||||||
storage.write("${type.value.name}_host", host.text));
|
|
||||||
port = TextEditingController(text: _readPort());
|
|
||||||
port.addListener(() =>
|
|
||||||
storage.write("${type.value.name}_port", port.text));
|
|
||||||
semaphore = Semaphore();
|
|
||||||
}
|
|
||||||
|
|
||||||
String get controllerName;
|
|
||||||
|
|
||||||
String get storageName;
|
|
||||||
|
|
||||||
String get defaultHost;
|
|
||||||
|
|
||||||
int get defaultPort;
|
|
||||||
|
|
||||||
Future<Uri?> pingServer(String host, int port);
|
|
||||||
|
|
||||||
Future<bool> get isPortFree;
|
|
||||||
|
|
||||||
Future<bool> get isPortTaken async => !(await isPortFree);
|
|
||||||
|
|
||||||
RebootPageType get pageType;
|
|
||||||
|
|
||||||
Future<bool> freePort();
|
|
||||||
|
|
||||||
@protected
|
|
||||||
Future<Process> startEmbeddedInternal();
|
|
||||||
|
|
||||||
void reset() async {
|
|
||||||
type.value = ServerType.values.elementAt(0);
|
|
||||||
for (final type in ServerType.values) {
|
|
||||||
storage.write("${type.name}_host", null);
|
|
||||||
storage.write("${type.name}_port", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
host.text = type.value != ServerType.remote ? defaultHost : "";
|
|
||||||
port.text = defaultPort.toString();
|
|
||||||
detached.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _readHost() {
|
|
||||||
String? value = storage.read("${type.value.name}_host");
|
|
||||||
return value != null && value.isNotEmpty ? value
|
|
||||||
: type.value != ServerType.remote ? defaultHost : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String _readPort() =>
|
|
||||||
storage.read("${type.value.name}_port") ?? defaultPort.toString();
|
|
||||||
|
|
||||||
Stream<ServerResult> start() async* {
|
|
||||||
try {
|
|
||||||
if(started.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final hostData = this.host.text.trim();
|
|
||||||
final portData = this.port.text.trim();
|
|
||||||
if(type() != ServerType.local) {
|
|
||||||
started.value = true;
|
|
||||||
yield ServerResult(ServerResultType.starting);
|
|
||||||
}else {
|
|
||||||
started.value = false;
|
|
||||||
if(portData != defaultPort.toString()) {
|
|
||||||
yield ServerResult(ServerResultType.starting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostData.isEmpty) {
|
|
||||||
yield ServerResult(ServerResultType.missingHostError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (portData.isEmpty) {
|
|
||||||
yield ServerResult(ServerResultType.missingPortError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final portNumber = int.tryParse(portData);
|
|
||||||
if (portNumber == null) {
|
|
||||||
yield ServerResult(ServerResultType.illegalPortError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((type() != ServerType.local || portData != defaultPort.toString()) && await isPortTaken) {
|
|
||||||
yield ServerResult(ServerResultType.freeingPort);
|
|
||||||
final result = await freePort();
|
|
||||||
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
|
|
||||||
if(!result) {
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(type()){
|
|
||||||
case ServerType.embedded:
|
|
||||||
final process = await startEmbeddedInternal();
|
|
||||||
final processPid = process.pid;
|
|
||||||
watchProcess(processPid).then((value) {
|
|
||||||
if(started()) {
|
|
||||||
started.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
yield ServerResult(ServerResultType.pingingRemote);
|
|
||||||
final uriResult = await pingServer(hostData, portNumber);
|
|
||||||
if(uriResult == null) {
|
|
||||||
yield ServerResult(ServerResultType.pingError);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteServer = await startRemoteBackendProxy(uriResult);
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
if(portData != defaultPort.toString()) {
|
|
||||||
localServer = await startRemoteBackendProxy(Uri.parse("http://$defaultHost:$portData"));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.pingingLocal);
|
|
||||||
final uriResult = await pingServer(defaultHost, defaultPort);
|
|
||||||
if(uriResult == null) {
|
|
||||||
yield ServerResult(ServerResultType.pingError);
|
|
||||||
remoteServer?.close(force: true);
|
|
||||||
localServer?.close(force: true);
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.startSuccess);
|
|
||||||
}catch(error, stackTrace) {
|
|
||||||
yield ServerResult(
|
|
||||||
ServerResultType.startError,
|
|
||||||
error: error,
|
|
||||||
stackTrace: stackTrace
|
|
||||||
);
|
|
||||||
remoteServer?.close(force: true);
|
|
||||||
localServer?.close(force: true);
|
|
||||||
started.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<ServerResult> stop() async* {
|
|
||||||
if(!started.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield ServerResult(ServerResultType.stopping);
|
|
||||||
started.value = false;
|
|
||||||
try{
|
|
||||||
switch(type()){
|
|
||||||
case ServerType.embedded:
|
|
||||||
killProcessByPort(defaultPort);
|
|
||||||
break;
|
|
||||||
case ServerType.remote:
|
|
||||||
await remoteServer?.close(force: true);
|
|
||||||
remoteServer = null;
|
|
||||||
break;
|
|
||||||
case ServerType.local:
|
|
||||||
await localServer?.close(force: true);
|
|
||||||
localServer = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
yield ServerResult(ServerResultType.stopSuccess);
|
|
||||||
}catch(error, stackTrace){
|
|
||||||
yield ServerResult(
|
|
||||||
ServerResultType.stopError,
|
|
||||||
error: error,
|
|
||||||
stackTrace: stackTrace
|
|
||||||
);
|
|
||||||
started.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<ServerResult> toggle() async* {
|
|
||||||
if(started()) {
|
|
||||||
yield* stop();
|
|
||||||
}else {
|
|
||||||
yield* start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:version/version.dart';
|
||||||
|
import 'package:yaml/yaml.dart';
|
||||||
|
|
||||||
class UpdateController {
|
class UpdateController {
|
||||||
late final GetStorage _storage;
|
late final GetStorage _storage;
|
||||||
@@ -11,6 +16,7 @@ class UpdateController {
|
|||||||
late final Rx<UpdateStatus> status;
|
late final Rx<UpdateStatus> status;
|
||||||
late final Rx<UpdateTimer> timer;
|
late final Rx<UpdateTimer> timer;
|
||||||
late final TextEditingController url;
|
late final TextEditingController url;
|
||||||
|
late final RxBool customGameServer;
|
||||||
InfoBarEntry? infoBarEntry;
|
InfoBarEntry? infoBarEntry;
|
||||||
Future? _updater;
|
Future? _updater;
|
||||||
|
|
||||||
@@ -19,25 +25,63 @@ class UpdateController {
|
|||||||
timestamp = RxnInt(_storage.read("ts"));
|
timestamp = RxnInt(_storage.read("ts"));
|
||||||
timestamp.listen((value) => _storage.write("ts", value));
|
timestamp.listen((value) => _storage.write("ts", value));
|
||||||
var timerIndex = _storage.read("timer");
|
var timerIndex = _storage.read("timer");
|
||||||
timer = Rx(timerIndex == null ? UpdateTimer.day : UpdateTimer.values.elementAt(timerIndex));
|
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
|
||||||
timer.listen((value) => _storage.write("timer", value.index));
|
timer.listen((value) => _storage.write("timer", value.index));
|
||||||
url = TextEditingController(text: _storage.read("update_url") ?? kRebootDownloadUrl);
|
url = TextEditingController(text: _storage.read("update_url") ?? kRebootDownloadUrl);
|
||||||
url.addListener(() => _storage.write("update_url", url.text));
|
url.addListener(() => _storage.write("update_url", url.text));
|
||||||
status = Rx(UpdateStatus.waiting);
|
status = Rx(UpdateStatus.waiting);
|
||||||
|
customGameServer = RxBool(_storage.read("custom_game_server") ?? false);
|
||||||
|
customGameServer.listen((value) => _storage.write("custom_game_server", value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> update([bool force = false]) async {
|
Future<void> notifyLauncherUpdate() async {
|
||||||
|
if(appVersion == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
|
||||||
|
if(pubspecResponse.statusCode != 200) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pubspec = loadYaml(pubspecResponse.body);
|
||||||
|
final latestVersion = Version.parse(pubspec["version"]);
|
||||||
|
if(latestVersion <= appVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
late InfoBarEntry infoBar;
|
||||||
|
infoBar = showInfoBar(
|
||||||
|
translations.updateAvailable(latestVersion.toString()),
|
||||||
|
duration: null,
|
||||||
|
severity: InfoBarSeverity.warning,
|
||||||
|
action: Button(
|
||||||
|
child: Text(translations.updateAvailableAction),
|
||||||
|
onPressed: () {
|
||||||
|
infoBar.close();
|
||||||
|
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateReboot([bool force = false]) async {
|
||||||
if(_updater != null) {
|
if(_updater != null) {
|
||||||
return await _updater;
|
return await _updater;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = _update(force);
|
final result = _updateReboot(force);
|
||||||
_updater = result;
|
_updater = result;
|
||||||
return await result;
|
return await result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _update([bool force = false]) async {
|
Future<void> _updateReboot([bool force = false]) async {
|
||||||
try {
|
try {
|
||||||
|
if(customGameServer.value) {
|
||||||
|
status.value = UpdateStatus.success;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final needsUpdate = await hasRebootDllUpdate(
|
final needsUpdate = await hasRebootDllUpdate(
|
||||||
timestamp.value,
|
timestamp.value,
|
||||||
hours: timer.value.hours,
|
hours: timer.value.hours,
|
||||||
@@ -72,7 +116,7 @@ class UpdateController {
|
|||||||
duration: infoBarLongDuration,
|
duration: infoBarLongDuration,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
action: Button(
|
action: Button(
|
||||||
onPressed: () => update(true),
|
onPressed: () => updateReboot(true),
|
||||||
child: Text(translations.downloadDllRetry),
|
child: Text(translations.downloadDllRetry),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -86,7 +130,8 @@ class UpdateController {
|
|||||||
timer.value = UpdateTimer.never;
|
timer.value = UpdateTimer.never;
|
||||||
url.text = kRebootDownloadUrl;
|
url.text = kRebootDownloadUrl;
|
||||||
status.value = UpdateStatus.waiting;
|
status.value = UpdateStatus.waiting;
|
||||||
update();
|
customGameServer.value = false;
|
||||||
|
updateReboot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
|||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.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/dialog_button.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
@@ -19,7 +18,9 @@ import 'package:reboot_launcher/src/util/translations.dart';
|
|||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:sync/semaphore.dart';
|
import 'package:sync/semaphore.dart';
|
||||||
|
|
||||||
extension ServerControllerDialog on ServerController {
|
final Semaphore _publishingSemaphore = Semaphore();
|
||||||
|
|
||||||
|
extension ServerControllerDialog on BackendController {
|
||||||
Future<bool> toggleInteractive() async {
|
Future<bool> toggleInteractive() async {
|
||||||
final stream = toggle();
|
final stream = toggle();
|
||||||
final completer = Completer<bool>();
|
final completer = Completer<bool>();
|
||||||
@@ -41,19 +42,19 @@ extension ServerControllerDialog on ServerController {
|
|||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case ServerResultType.starting:
|
case ServerResultType.starting:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.startingServer(controllerName),
|
translations.startingServer,
|
||||||
severity: InfoBarSeverity.info,
|
severity: InfoBarSeverity.info,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.startSuccess:
|
case ServerResultType.startSuccess:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
type.value == ServerType.local ? translations.checkedServer(controllerName) : translations.startedServer(controllerName),
|
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
);
|
);
|
||||||
case ServerResultType.startError:
|
case ServerResultType.startError:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError, controllerName) : translations.startServerError(event.error ?? translations.unknownError, controllerName),
|
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
@@ -66,75 +67,70 @@ extension ServerControllerDialog on ServerController {
|
|||||||
);
|
);
|
||||||
case ServerResultType.stopSuccess:
|
case ServerResultType.stopSuccess:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.stoppedServer(controllerName),
|
translations.stoppedServer,
|
||||||
severity: InfoBarSeverity.success
|
severity: InfoBarSeverity.success
|
||||||
);
|
);
|
||||||
case ServerResultType.stopError:
|
case ServerResultType.stopError:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.stopServerError(
|
translations.stopServerError(event.error ?? translations.unknownError),
|
||||||
event.error ?? translations.unknownError, controllerName),
|
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
case ServerResultType.missingHostError:
|
case ServerResultType.missingHostError:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.missingHostNameError(controllerName),
|
translations.missingHostNameError,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
case ServerResultType.missingPortError:
|
case ServerResultType.missingPortError:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.missingPortError(controllerName),
|
translations.missingPortError,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
case ServerResultType.illegalPortError:
|
case ServerResultType.illegalPortError:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.illegalPortError(controllerName),
|
translations.illegalPortError,
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
case ServerResultType.freeingPort:
|
case ServerResultType.freeingPort:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.freeingPort(defaultPort),
|
translations.freeingPort,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.freePortSuccess:
|
case ServerResultType.freePortSuccess:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.freedPort(defaultPort),
|
translations.freedPort,
|
||||||
severity: InfoBarSeverity.success,
|
severity: InfoBarSeverity.success,
|
||||||
duration: infoBarShortDuration
|
duration: infoBarShortDuration
|
||||||
);
|
);
|
||||||
case ServerResultType.freePortError:
|
case ServerResultType.freePortError:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.freePortError(event.error ?? translations.unknownError, controllerName),
|
translations.freePortError(event.error ?? translations.unknownError),
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
duration: infoBarLongDuration
|
duration: infoBarLongDuration
|
||||||
);
|
);
|
||||||
case ServerResultType.pingingRemote:
|
case ServerResultType.pingingRemote:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.pingingRemoteServer(controllerName),
|
translations.pingingRemoteServer,
|
||||||
severity: InfoBarSeverity.info,
|
severity: InfoBarSeverity.info,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.pingingLocal:
|
case ServerResultType.pingingLocal:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.pingingLocalServer(controllerName, type().name),
|
translations.pingingLocalServer(type.value.name),
|
||||||
severity: InfoBarSeverity.info,
|
severity: InfoBarSeverity.info,
|
||||||
loading: true,
|
loading: true,
|
||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
case ServerResultType.pingError:
|
case ServerResultType.pingError:
|
||||||
return showInfoBar(
|
return showInfoBar(
|
||||||
translations.pingError(controllerName, type().name),
|
translations.pingError(type.value.name),
|
||||||
severity: InfoBarSeverity.error
|
severity: InfoBarSeverity.error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
final Semaphore _publishingSemaphore = Semaphore();
|
|
||||||
|
|
||||||
extension MatchmakerControllerExtension on MatchmakerController {
|
|
||||||
void joinLocalHost() {
|
void joinLocalHost() {
|
||||||
gameServerAddress.text = kDefaultGameServerHost;
|
gameServerAddress.text = kDefaultGameServerHost;
|
||||||
gameServerOwner.value = null;
|
gameServerOwner.value = null;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ enum RebootPageType {
|
|||||||
host,
|
host,
|
||||||
browser,
|
browser,
|
||||||
backend,
|
backend,
|
||||||
matchmaker,
|
|
||||||
info,
|
info,
|
||||||
settings
|
settings
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.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/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/server_start_button.dart';
|
import 'package:reboot_launcher/src/widget/server_start_button.dart';
|
||||||
@@ -67,12 +66,32 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
_type,
|
_type,
|
||||||
_hostName,
|
_hostName,
|
||||||
_port,
|
_port,
|
||||||
_detached,
|
_gameServerAddress,
|
||||||
_unrealEngineConsoleKey,
|
_unrealEngineConsoleKey,
|
||||||
|
_detached,
|
||||||
_installationDirectory,
|
_installationDirectory,
|
||||||
_resetDefaults
|
_resetDefaults
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Widget get _gameServerAddress => Obx(() {
|
||||||
|
if(_backendController.type.value != ServerType.embedded) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.stream_input_20_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.matchmakerConfigurationAddressName),
|
||||||
|
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: translations.matchmakerConfigurationAddressName,
|
||||||
|
controller: _backendController.gameServerAddress,
|
||||||
|
focusNode: _backendController.gameServerAddressFocusNode
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Widget get _hostName => Obx(() {
|
Widget get _hostName => Obx(() {
|
||||||
if(_backendController.type.value != ServerType.remote) {
|
if(_backendController.type.value != ServerType.remote) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -202,13 +221,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
|||||||
),
|
),
|
||||||
title: Text(translations.backendTypeName),
|
title: Text(translations.backendTypeName),
|
||||||
subtitle: Text(translations.backendTypeDescription),
|
subtitle: Text(translations.backendTypeDescription),
|
||||||
content: const ServerTypeSelector(
|
content: const ServerTypeSelector()
|
||||||
backend: true
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get button => const ServerButton(
|
Widget get button => const ServerButton();
|
||||||
backend: true
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
|||||||
void initState() {
|
void initState() {
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_updateController.update();
|
_updateController.notifyLauncherUpdate();
|
||||||
|
_updateController.updateReboot();
|
||||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||||
downloadCriticalDllInteractive(filePath);
|
downloadCriticalDllInteractive(filePath);
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ 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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.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:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
|
||||||
|
|
||||||
class InfoPage extends RebootPage {
|
class InfoPage extends RebootPage {
|
||||||
const InfoPage({Key? key}) : super(key: key);
|
const InfoPage({Key? key}) : super(key: key);
|
||||||
@@ -32,153 +32,6 @@ class _InfoPageState extends RebootPageState<InfoPage> {
|
|||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
RxInt _counter = RxInt(180);
|
RxInt _counter = RxInt(180);
|
||||||
|
|
||||||
static final List<InfoTile> _infoTiles = [
|
|
||||||
InfoTile(
|
|
||||||
title: Text("What is Project Reboot?"),
|
|
||||||
content: Text(
|
|
||||||
"Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.\n"
|
|
||||||
"The project was started on Discord by Milxnor, while the launcher is developed by Auties00.\n"
|
|
||||||
"Both are open source on GitHub, anyone can easily contribute or audit the code!"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("What is a Fortnite game server?"),
|
|
||||||
content: Text(
|
|
||||||
"If you have ever played Minecraft multiplayer, you might know that the servers you join are hosted on a computer running a program, called Minecraft Game Server.\n"
|
|
||||||
"While the Minecraft Game server is written by the creators of Minecraft, Mojang, Epic Games doesn't provide an equivalent for Fortnite.\n"
|
|
||||||
"By exploiting the Fortnite internals, though, it's possible to create a game server just like in Minecraft: this is in easy terms what Project Reboot does.\n"
|
|
||||||
"Some Fortnite versions support running this game server in the background without rendering the game(\"headless\"), while others still require the full game to be open.\n"
|
|
||||||
"Just like in Minecraft, you need a game client to play the game and one to host the server.\n"
|
|
||||||
"By default, a game server is automatically started on your PC when you start a Fortnite version from the \"Play\" section in the launcher.\n"
|
|
||||||
"If you want to play in another way, for example by joining a server hosted by one of your friends instead of running one yourself, you can checkout the \"Multiplayer\" section in the \"Play\" tab of the launcher."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("Types of Fortnite game server"),
|
|
||||||
content: Text(
|
|
||||||
"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.\n"
|
|
||||||
"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.\n"
|
|
||||||
"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."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("How can others join my game server?"),
|
|
||||||
content: Text(
|
|
||||||
"For others to join your game server, port 7777 must be accessible on your PC.\n"
|
|
||||||
"One option is to use a private VPN service like Hamachi or Radmin, but all of the players will need to download this software.\n"
|
|
||||||
"The best solution is to use port forwarding:\n"
|
|
||||||
"1. Set a static IP\n"
|
|
||||||
" If you don't have already a static IP set, set one by following any tutorial on Google\n"
|
|
||||||
"2. Log into your router's admin panel\n"
|
|
||||||
" Usually this can be accessed on any web browser by going to http://192.168.1.1/\n"
|
|
||||||
" You might need a username and a password to log in: refer to your router's manual for precise instructions\n"
|
|
||||||
"3. Find the port forwarding section\n"
|
|
||||||
" Once logged in into the admin panel, navigate to the port forwarding section of your router's settings\n"
|
|
||||||
" This location may vary from router to router, but it's typically labelled as \"Port Forwarding,\" \"Port Mapping\" or \"Virtual Server\"\n"
|
|
||||||
" Refer to your router's manual for precise instructions\n"
|
|
||||||
"4. Add a port forwarding rule\n"
|
|
||||||
" Now, you'll need to create a new port forwarding rule. Here's what you'll typically need to specify:\n"
|
|
||||||
" - Service Name: Choose a name for your port forwarding rule (e.g., \"Fortnite Game Server\")\n"
|
|
||||||
" - Port Number: Enter 7777 for both the external and internal ports\n"
|
|
||||||
" - Protocol: Select the UDP protocol\n"
|
|
||||||
" - Internal IP Address: Enter the static IP address you set earlier\n"
|
|
||||||
" - Enable: Make sure the port forwarding rule is enabled\n"
|
|
||||||
"5. Save and apply the changes\n"
|
|
||||||
" After configuring the port forwarding rule, save your changes and apply them\n"
|
|
||||||
" This step may involve clicking a \"Save\" or \"Apply\" button on your router's web interface"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("What is a backend?"),
|
|
||||||
content: Text(
|
|
||||||
"A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features.\n"
|
|
||||||
"By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github.\n"
|
|
||||||
"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.\n"
|
|
||||||
"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.\n"
|
|
||||||
"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."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("What is the Unreal Engine console?"),
|
|
||||||
content: Text(
|
|
||||||
"Many Fortnite versions don't support entering in game by clicking the \"Play\" button.\n"
|
|
||||||
"Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1\n"
|
|
||||||
"Keep in mind that the Unreal Engine console key is controlled by the backend, so this is true only if you are using the embedded backend: custom backends might use different keys.\n"
|
|
||||||
"When using the embedded backend, you can customize the key used to open the console in the \"Backend\" tab of the Reboot Launcher."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("What is a matchmaker?"),
|
|
||||||
content: Text(
|
|
||||||
"A matchmaker is a piece of software that emulates the Epic Games server responsible for putting you in game when you click the \"Play\" button in Fortnite.\n"
|
|
||||||
"By default, the Reboot Launcher ships with a slightly customized version of Lawin's FortMatchmaker, an open source implementation available on Github.\n"
|
|
||||||
"If you are having any problems with the built in matchmaker, enable the \"Detached\" option in the \"Matchmaker\" tab of the Reboot Launcher to troubleshoot the issue.\n"
|
|
||||||
"Lawin's FortMatchmaker is an extremely basic implementation of a matchmaker: it takes the IP you configured in the \"Matchmaker\" tab, by default 127.0.0.1(your local machine) of the Reboot Launcher and send you with no wait to that game server.\n"
|
|
||||||
"Unfortunately right now the play button still doesn't work on many Fortnite versions, you so might need to use the Unreal Engine console.\n"
|
|
||||||
"Just like a backend, you can run a custom matchmaker, either on your PC or on a server with the appropriate configuration."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("The backend is not working correctly"),
|
|
||||||
content: Text(
|
|
||||||
"To resolve this issue:\n"
|
|
||||||
"- Check that your backend is working correctly from the \"Backend\" tab\n"
|
|
||||||
"- If you are using a custom backend, try to use the embedded one\n"
|
|
||||||
"- Try to run the backend as detached by enabling the \"Detached\" option in the \"Backend\" tab"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("The matchmaker is not working correctly"),
|
|
||||||
content: Text(
|
|
||||||
"To resolve this issue:\n"
|
|
||||||
"- Check that your matchmaker is working correctly from the \"Matchmaker\" tab\n"
|
|
||||||
"- If you are using a custom matchmaker, try to use the embedded one\n"
|
|
||||||
"- Try to run the matchmaker as detached by enabling the \"Detached\" option in the \"Matchmaker\" tab"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("Why do I see two Fortnite versions opened on my PC?"),
|
|
||||||
content: Text(
|
|
||||||
"As explained in the \"What is a Fortnite game server?\" section, one instance of Fortnite is used to host the game server, while the other is used to let you play.\n"
|
|
||||||
"The Fortnite instance used up by the game server is usually frozen, so it should be hard to use the wrong one to try to play.\n"
|
|
||||||
"If you do not want to host a game server yourself, you can:\n"
|
|
||||||
"1. Set a custom IP in the \"Matchmaker\" tab\n"
|
|
||||||
"2. Set a custom matchmaker in the \"Matchmaker\" tab\n"
|
|
||||||
"3. Disable the automatic game server from the \"Configuration\" section in the \"Host\" tab\n"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("I cannot open Fortnite because of an authentication error"),
|
|
||||||
content: Text(
|
|
||||||
"To resolve this issue:\n"
|
|
||||||
"- Check that your backend is working correctly from the \"Backend\" tab\n"
|
|
||||||
"- If you are using a custom backend, try to use the embedded one\n"
|
|
||||||
"- Try to run the backend as detached by enabling the \"Detached\" option in the \"Backend\" tab"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("I cannot enter in a match when I'm in Fortnite"),
|
|
||||||
content: Text(
|
|
||||||
"As explained in the \"What is the Unreal Engine console?\" section, the \"Play\" button doesn't work in many Fortnite versions.\n"
|
|
||||||
"Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("An error occurred while downloading a build (DioException)"),
|
|
||||||
content: Text(
|
|
||||||
"Unfortunately the servers that host the Fortnite builds are not reliable all the time so it might take a few tries, or downloading another version, to get started"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
InfoTile(
|
|
||||||
title: Text("Failed to open descriptor file / Fortnite crash Reporter / Unreal Engine crash reporter"),
|
|
||||||
content: Text(
|
|
||||||
"Your version of Fortnite is corrupted, download it again from the launcher or use another build."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
if(_settingsController.firstRun.value) {
|
if(_settingsController.firstRun.value) {
|
||||||
@@ -195,7 +48,7 @@ class _InfoPageState extends RebootPageState<InfoPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Widget> get settings => _infoTiles;
|
List<Widget> get settings => infoTiles;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? get button => Obx(() {
|
Widget? get button => Obx(() {
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.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';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/server_start_button.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class MatchmakerPage extends RebootPage {
|
|
||||||
const MatchmakerPage({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
RebootPageState<MatchmakerPage> createState() => _MatchmakerPageState();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get name => translations.matchmakerName;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get iconAsset => "assets/images/matchmaker.png";
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool hasButton(String? pageName) => pageName == null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RebootPageType get type => RebootPageType.matchmaker;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MatchmakerPageState extends RebootPageState<MatchmakerPage> {
|
|
||||||
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget? get button => const ServerButton(
|
|
||||||
backend: false
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Widget> get settings => [
|
|
||||||
_type,
|
|
||||||
_hostName,
|
|
||||||
_port,
|
|
||||||
_gameServerAddress,
|
|
||||||
_installationDirectory,
|
|
||||||
_resetDefaults
|
|
||||||
];
|
|
||||||
|
|
||||||
Widget get _gameServerAddress => Obx(() {
|
|
||||||
if(_matchmakerController.type.value != ServerType.embedded) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.stream_input_20_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.matchmakerConfigurationAddressName),
|
|
||||||
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: translations.matchmakerConfigurationAddressName,
|
|
||||||
controller: _matchmakerController.gameServerAddress,
|
|
||||||
focusNode: _matchmakerController.gameServerAddressFocusNode
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget get _port => Obx(() {
|
|
||||||
if(_matchmakerController.type.value == ServerType.embedded) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
fluentUi.FluentIcons.number_field
|
|
||||||
),
|
|
||||||
title: Text(translations.matchmakerConfigurationPortName),
|
|
||||||
subtitle: Text(translations.matchmakerConfigurationPortDescription),
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: translations.matchmakerConfigurationPortName,
|
|
||||||
controller: _matchmakerController.port,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [
|
|
||||||
FilteringTextInputFormatter.digitsOnly
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget get _hostName => Obx(() {
|
|
||||||
if(_matchmakerController.type.value != ServerType.remote) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.globe_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.matchmakerConfigurationHostName),
|
|
||||||
subtitle: Text(translations.matchmakerConfigurationHostDescription),
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: translations.matchmakerConfigurationHostName,
|
|
||||||
controller: _matchmakerController.host
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget get _type => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.people_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.matchmakerTypeName),
|
|
||||||
subtitle: Text(translations.matchmakerTypeDescription),
|
|
||||||
content: const ServerTypeSelector(
|
|
||||||
backend: false
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget get _installationDirectory => Obx(() {
|
|
||||||
if(_matchmakerController.type.value != ServerType.embedded) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.folder_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.matchmakerInstallationDirectoryName),
|
|
||||||
subtitle: Text(translations.matchmakerInstallationDirectoryDescription),
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => launchUrl(matchmakerDirectory.uri),
|
|
||||||
child: Text(translations.matchmakerInstallationDirectoryContent)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
SettingTile get _resetDefaults => SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.arrow_reset_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.matchmakerResetDefaultsName),
|
|
||||||
subtitle: Text(translations.matchmakerResetDefaultsDescription),
|
|
||||||
content: Button(
|
|
||||||
onPressed: () => showResetDialog(_matchmakerController.reset),
|
|
||||||
child: Text(translations.matchmakerResetDefaultsContent),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.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/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
import 'package:reboot_launcher/src/page/pages.dart';
|
||||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||||
@@ -39,9 +34,9 @@ class PlayPage extends RebootPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||||
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? get button => LaunchButton(
|
Widget? get button => LaunchButton(
|
||||||
@@ -52,8 +47,9 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<SettingTile> get settings => [
|
List<SettingTile> get settings => [
|
||||||
_clientSettings,
|
|
||||||
versionSelectSettingTile,
|
versionSelectSettingTile,
|
||||||
|
_options,
|
||||||
|
_internalFiles,
|
||||||
_multiplayer
|
_multiplayer
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -70,7 +66,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
SettingTile get _clientSettings => SettingTile(
|
SettingTile get _internalFiles => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.archive_settings_24_regular
|
FluentIcons.archive_settings_24_regular
|
||||||
),
|
),
|
||||||
@@ -92,24 +88,34 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
|||||||
description: translations.settingsClientMemoryDescription,
|
description: translations.settingsClientMemoryDescription,
|
||||||
controller: _settingsController.memoryLeakDll
|
controller: _settingsController.memoryLeakDll
|
||||||
),
|
),
|
||||||
SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.options_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsClientArgsName),
|
|
||||||
subtitle: Text(translations.settingsClientArgsDescription),
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: translations.settingsClientArgsPlaceholder,
|
|
||||||
controller: _gameController.customLaunchArgs,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
SettingTile get _options => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.options_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerOptionsName),
|
||||||
|
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||||
|
children: [
|
||||||
|
SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.options_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsClientArgsName),
|
||||||
|
subtitle: Text(translations.settingsClientArgsDescription),
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: translations.settingsClientArgsPlaceholder,
|
||||||
|
controller: _gameController.customLaunchArgs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
SettingTile get _matchmakerTile => SettingTile(
|
SettingTile get _matchmakerTile => SettingTile(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
pageIndex.value = RebootPageType.matchmaker.index;
|
pageIndex.value = RebootPageType.backend.index;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _matchmakerController.gameServerAddressFocusNode.requestFocus());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _backendController.gameServerAddressFocusNode.requestFocus());
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.globe_24_regular
|
FluentIcons.globe_24_regular
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/dialog/implementation/server.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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||||
@@ -34,7 +34,7 @@ class BrowsePage extends RebootPage {
|
|||||||
|
|
||||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
final TextEditingController _filterController = TextEditingController();
|
final TextEditingController _filterController = TextEditingController();
|
||||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||||
|
|
||||||
@@ -98,11 +98,11 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
|
|||||||
title: Text("${_formatName(entry)} • ${entry["author"]}"),
|
title: Text("${_formatName(entry)} • ${entry["author"]}"),
|
||||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||||
content: Button(
|
content: Button(
|
||||||
onPressed: () => _matchmakerController.joinServer(_hostingController.uuid, entry),
|
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(_matchmakerController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
import 'package:dart_ipify/dart_ipify.dart';
|
import 'package:dart_ipify/dart_ipify.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -20,7 +21,6 @@ import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
|||||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart' as fluentUi show FluentIcons;
|
|
||||||
|
|
||||||
import '../../util/checks.dart';
|
import '../../util/checks.dart';
|
||||||
|
|
||||||
@@ -72,8 +72,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
@override
|
@override
|
||||||
List<Widget> get settings => [
|
List<Widget> get settings => [
|
||||||
_information,
|
_information,
|
||||||
_configuration,
|
|
||||||
versionSelectSettingTile,
|
versionSelectSettingTile,
|
||||||
|
_options,
|
||||||
|
_internalFiles,
|
||||||
_share,
|
_share,
|
||||||
_resetDefaults
|
_resetDefaults
|
||||||
];
|
];
|
||||||
@@ -179,63 +180,104 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
SettingTile get _configuration => SettingTile(
|
SettingTile get _internalFiles => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.archive_settings_24_regular
|
FluentIcons.archive_settings_24_regular
|
||||||
),
|
),
|
||||||
title: Text(translations.settingsServerName),
|
title: Text(translations.settingsServerName),
|
||||||
subtitle: Text(translations.settingsServerSubtitle),
|
subtitle: Text(translations.settingsServerSubtitle),
|
||||||
children: [
|
children: [
|
||||||
createFileSetting(
|
|
||||||
title: translations.settingsServerFileName,
|
|
||||||
description: translations.settingsServerFileDescription,
|
|
||||||
controller: _settingsController.gameServerDll
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
fluentUi.FluentIcons.number_field
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsServerPortName),
|
|
||||||
subtitle: Text(translations.settingsServerPortDescription),
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: translations.settingsServerPortName,
|
|
||||||
controller: _settingsController.gameServerPort,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [
|
|
||||||
FilteringTextInputFormatter.digitsOnly
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
SettingTile(
|
|
||||||
icon: Icon(
|
|
||||||
FluentIcons.globe_24_regular
|
|
||||||
),
|
|
||||||
title: Text(translations.settingsServerMirrorName),
|
|
||||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
|
||||||
content: TextFormBox(
|
|
||||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
|
||||||
controller: _updateController.url,
|
|
||||||
validator: checkUpdateUrl
|
|
||||||
)
|
|
||||||
),
|
|
||||||
SettingTile(
|
SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.timer_24_regular
|
FluentIcons.timer_24_regular
|
||||||
),
|
),
|
||||||
title: Text(translations.settingsServerTimerName),
|
title: Text(translations.settingsServerTypeName),
|
||||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
subtitle: Text(translations.settingsServerTypeDescription),
|
||||||
content: Obx(() => DropDownButton(
|
content: Obx(() => DropDownButton(
|
||||||
leading: Text(_updateController.timer.value.text),
|
leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
items: {
|
||||||
text: Text(entry.text),
|
false: translations.settingsServerTypeEmbeddedName,
|
||||||
|
true: translations.settingsServerTypeCustomName
|
||||||
|
}.entries.map((entry) => MenuFlyoutItem(
|
||||||
|
text: Text(entry.value),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_updateController.timer.value = entry;
|
final oldValue = _updateController.customGameServer.value;
|
||||||
|
if(oldValue == entry.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateController.customGameServer.value = entry.key;
|
||||||
_updateController.infoBarEntry?.close();
|
_updateController.infoBarEntry?.close();
|
||||||
_updateController.update(true);
|
if(!entry.key) {
|
||||||
|
_updateController.updateReboot(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)).toList()
|
)).toList()
|
||||||
))
|
))
|
||||||
),
|
),
|
||||||
|
Obx(() {
|
||||||
|
if(!_updateController.customGameServer.value) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return createFileSetting(
|
||||||
|
title: translations.settingsServerFileName,
|
||||||
|
description: translations.settingsServerFileDescription,
|
||||||
|
controller: _settingsController.gameServerDll
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Obx(() {
|
||||||
|
if(_updateController.customGameServer.value) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.globe_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerMirrorName),
|
||||||
|
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||||
|
controller: _updateController.url,
|
||||||
|
validator: checkUpdateUrl
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Obx(() {
|
||||||
|
if(_updateController.customGameServer.value) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.timer_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerTimerName),
|
||||||
|
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||||
|
content: Obx(() => DropDownButton(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
)).toList()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
SettingTile get _options => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.options_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerOptionsName),
|
||||||
|
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||||
|
children: [
|
||||||
Obx(() => SettingTile(
|
Obx(() => SettingTile(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FluentIcons.window_console_20_regular
|
FluentIcons.window_console_20_regular
|
||||||
@@ -280,6 +322,45 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
Obx(() => SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
FluentIcons.arrow_reset_24_regular
|
||||||
|
),
|
||||||
|
title: Text(translations.hostAutomaticRestartName),
|
||||||
|
subtitle: Text(translations.hostAutomaticRestartDescription),
|
||||||
|
contentWidth: null,
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_hostingController.autoRestart.value ? translations.on : translations.off
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16.0
|
||||||
|
),
|
||||||
|
ToggleSwitch(
|
||||||
|
checked: _hostingController.autoRestart.value,
|
||||||
|
onChanged: (value) => _hostingController.autoRestart.value = value
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
SettingTile(
|
||||||
|
icon: Icon(
|
||||||
|
fluentUi.FluentIcons.number_field
|
||||||
|
),
|
||||||
|
title: Text(translations.settingsServerPortName),
|
||||||
|
subtitle: Text(translations.settingsServerPortDescription),
|
||||||
|
contentWidth: 64,
|
||||||
|
content: TextFormBox(
|
||||||
|
placeholder: translations.settingsServerPortName,
|
||||||
|
controller: _settingsController.gameServerPort,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
|
||||||
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.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/data.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page.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/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/util/picker.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.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.dart';
|
||||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
import 'package:reboot_launcher/src/page/implementation/backend_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/matchmaker_page.dart';
|
|
||||||
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
import 'package:reboot_launcher/src/page/implementation/play_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart';
|
import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/implementation/server_host_page.dart';
|
import 'package:reboot_launcher/src/page/implementation/server_host_page.dart';
|
||||||
@@ -24,7 +22,6 @@ final List<RebootPage> pages = [
|
|||||||
const HostPage(),
|
const HostPage(),
|
||||||
const BrowsePage(),
|
const BrowsePage(),
|
||||||
const BackendPage(),
|
const BackendPage(),
|
||||||
const MatchmakerPage(),
|
|
||||||
const InfoPage(),
|
const InfoPage(),
|
||||||
const SettingsPage()
|
const SettingsPage()
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Future<void> _downloadCriticalDllInteractive(String filePath) async {
|
|||||||
InfoBarEntry? entry;
|
InfoBarEntry? entry;
|
||||||
try {
|
try {
|
||||||
if (fileName == "reboot.dll") {
|
if (fileName == "reboot.dll") {
|
||||||
await _updateController.update(true);
|
await _updateController.updateReboot(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
gui/lib/src/util/info.dart
Normal file
41
gui/lib/src/util/info.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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);
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:win32/win32.dart';
|
import 'package:win32/win32.dart';
|
||||||
|
|
||||||
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||||
@@ -38,15 +38,15 @@ class _ServiceProvider10 extends IUnknown {
|
|||||||
Pointer<COMObject> queryService(String classId, String instanceId) {
|
Pointer<COMObject> queryService(String classId, String instanceId) {
|
||||||
final result = calloc<COMObject>();
|
final result = calloc<COMObject>();
|
||||||
final code = (ptr.ref.vtable + 3)
|
final code = (ptr.ref.vtable + 3)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
HRESULT Function(Pointer, Pointer<GUID>, Pointer<GUID>,
|
HRESULT Function(Pointer, Pointer<GUID>, Pointer<GUID>,
|
||||||
Pointer<COMObject>)>>>()
|
Pointer<COMObject>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<
|
.asFunction<
|
||||||
int Function(Pointer, Pointer<GUID>, Pointer<GUID>,
|
int Function(Pointer, Pointer<GUID>, Pointer<GUID>,
|
||||||
Pointer<COMObject>)>()(ptr.ref.lpVtbl,
|
Pointer<COMObject>)>()(ptr.ref.lpVtbl,
|
||||||
GUIDFromString(classId), GUIDFromString(instanceId), result);
|
GUIDFromString(classId), GUIDFromString(instanceId), result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
@@ -66,11 +66,11 @@ class IVirtualDesktop extends IUnknown {
|
|||||||
final result = calloc<HSTRING>();
|
final result = calloc<HSTRING>();
|
||||||
final code = (ptr.ref.vtable + 5)
|
final code = (ptr.ref.vtable + 5)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<HRESULT Function(Pointer, Pointer<HSTRING>)>>>()
|
NativeFunction<HRESULT Function(Pointer, Pointer<HSTRING>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<
|
.asFunction<
|
||||||
int Function(Pointer, Pointer<HSTRING>)>()(ptr.ref.lpVtbl, result);
|
int Function(Pointer, Pointer<HSTRING>)>()(ptr.ref.lpVtbl, result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
@@ -93,11 +93,11 @@ class _IObjectArray extends IUnknown {
|
|||||||
final result = calloc<Int32>();
|
final result = calloc<Int32>();
|
||||||
final code = (ptr.ref.vtable + 3)
|
final code = (ptr.ref.vtable + 3)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
|
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<
|
.asFunction<
|
||||||
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
|
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
@@ -109,15 +109,15 @@ class _IObjectArray extends IUnknown {
|
|||||||
Pointer<COMObject> getAt(int index, String guid) {
|
Pointer<COMObject> getAt(int index, String guid) {
|
||||||
final result = calloc<COMObject>();
|
final result = calloc<COMObject>();
|
||||||
final code = (ptr.ref.vtable + 4)
|
final code = (ptr.ref.vtable + 4)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
HRESULT Function(Pointer, Int32 index, Pointer<GUID>,
|
HRESULT Function(Pointer, Int32 index, Pointer<GUID>,
|
||||||
Pointer<COMObject>)>>>()
|
Pointer<COMObject>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<
|
.asFunction<
|
||||||
int Function(
|
int Function(
|
||||||
Pointer, int index, Pointer<GUID>, Pointer<COMObject>)>()(
|
Pointer, int index, Pointer<GUID>, Pointer<COMObject>)>()(
|
||||||
ptr.ref.lpVtbl, index, GUIDFromString(guid), result);
|
ptr.ref.lpVtbl, index, GUIDFromString(guid), result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
@@ -137,8 +137,8 @@ class _IObjectArrayList<T> extends ListBase<T> {
|
|||||||
|
|
||||||
_IObjectArrayList(
|
_IObjectArrayList(
|
||||||
{required _IObjectArray array,
|
{required _IObjectArray array,
|
||||||
required String guid,
|
required String guid,
|
||||||
required _IObjectMapper<T> mapper})
|
required _IObjectMapper<T> mapper})
|
||||||
: _array = array,
|
: _array = array,
|
||||||
_guid = guid,
|
_guid = guid,
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
@@ -173,11 +173,11 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
|||||||
final result = calloc<Int32>();
|
final result = calloc<Int32>();
|
||||||
final code = (ptr.ref.vtable + 3)
|
final code = (ptr.ref.vtable + 3)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
|
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<
|
.asFunction<
|
||||||
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
|
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
@@ -189,12 +189,12 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
|||||||
List<IVirtualDesktop> getDesktops() {
|
List<IVirtualDesktop> getDesktops() {
|
||||||
final result = calloc<COMObject>();
|
final result = calloc<COMObject>();
|
||||||
final code = (ptr.ref.vtable + 7)
|
final code = (ptr.ref.vtable + 7)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
|
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
|
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
|
||||||
ptr.ref.lpVtbl, result);
|
ptr.ref.lpVtbl, result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
@@ -210,27 +210,27 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
|||||||
|
|
||||||
void moveWindowToDesktop(IApplicationView view, IVirtualDesktop desktop) {
|
void moveWindowToDesktop(IApplicationView view, IVirtualDesktop desktop) {
|
||||||
final code = (ptr.ref.vtable + 4)
|
final code = (ptr.ref.vtable + 4)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
Int32 Function(Pointer, COMObject, COMObject)>>>()
|
Int32 Function(Pointer, COMObject, COMObject)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
|
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
|
||||||
ptr.ref.lpVtbl, view.ptr.ref, desktop.ptr.ref);
|
ptr.ref.lpVtbl, view.ptr.ref, desktop.ptr.ref);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
throw WindowsException(code);
|
throw WindowsException(code, message: "Cannot move window");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IVirtualDesktop createDesktop() {
|
IVirtualDesktop createDesktop() {
|
||||||
final result = calloc<COMObject>();
|
final result = calloc<COMObject>();
|
||||||
final code = (ptr.ref.vtable + 10)
|
final code = (ptr.ref.vtable + 10)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
|
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
|
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
|
||||||
ptr.ref.lpVtbl, result);
|
ptr.ref.lpVtbl, result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
@@ -242,12 +242,12 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
|||||||
|
|
||||||
void removeDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback) {
|
void removeDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback) {
|
||||||
final code = (ptr.ref.vtable + 12)
|
final code = (ptr.ref.vtable + 12)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
HRESULT Function(Pointer, COMObject, COMObject)>>>()
|
HRESULT Function(Pointer, COMObject, COMObject)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
|
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
|
||||||
ptr.ref.lpVtbl, desktop.ptr.ref, fallback.ptr.ref);
|
ptr.ref.lpVtbl, desktop.ptr.ref, fallback.ptr.ref);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
@@ -256,14 +256,14 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
|
|||||||
|
|
||||||
void setDesktopName(IVirtualDesktop desktop, String newName) {
|
void setDesktopName(IVirtualDesktop desktop, String newName) {
|
||||||
final code =
|
final code =
|
||||||
(ptr.ref.vtable + 15)
|
(ptr.ref.vtable + 15)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
HRESULT Function(Pointer, COMObject, Int8)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, COMObject, int)>()(
|
.asFunction<int Function(Pointer, COMObject, int)>()(
|
||||||
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName));
|
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName));
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
throw WindowsException(code);
|
throw WindowsException(code);
|
||||||
}
|
}
|
||||||
@@ -276,57 +276,75 @@ class _IApplicationViewCollection extends IUnknown {
|
|||||||
|
|
||||||
_IApplicationViewCollection._internal(super.ptr);
|
_IApplicationViewCollection._internal(super.ptr);
|
||||||
|
|
||||||
IApplicationView getViewForHWnd(int HWnd) {
|
IApplicationView? getViewForHWnd(int HWnd) {
|
||||||
final result = calloc<COMObject>();
|
final result = calloc<COMObject>();
|
||||||
final code =
|
final code =
|
||||||
(ptr.ref.vtable + 6)
|
(ptr.ref.vtable + 6)
|
||||||
.cast<
|
.cast<
|
||||||
Pointer<
|
Pointer<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
HRESULT Function(
|
HRESULT Function(
|
||||||
Pointer, IntPtr, Pointer<COMObject>)>>>()
|
Pointer, IntPtr, Pointer<COMObject>)>>>()
|
||||||
.value
|
.value
|
||||||
.asFunction<int Function(Pointer, int, Pointer<COMObject>)>()(
|
.asFunction<int Function(Pointer, int, Pointer<COMObject>)>()(
|
||||||
ptr.ref.lpVtbl, HWnd, result);
|
ptr.ref.lpVtbl, HWnd, result);
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
free(result);
|
free(result);
|
||||||
throw WindowsException(code);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IApplicationView._internal(result);
|
return IApplicationView._internal(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _Process extends Struct {
|
final class Win32Process extends Struct {
|
||||||
@Uint32()
|
@Uint32()
|
||||||
external int pid;
|
external int pid;
|
||||||
|
|
||||||
@Uint32()
|
@Uint32()
|
||||||
external int HWnd;
|
external int HWndLength;
|
||||||
|
|
||||||
static int _filter(int HWnd, int lParam) {
|
external Pointer<Uint32> HWnd;
|
||||||
final structure = Pointer.fromAddress(lParam).cast<_Process>();
|
}
|
||||||
final pidPointer = calloc<Uint32>();
|
|
||||||
GetWindowThreadProcessId(HWnd, pidPointer);
|
int _filter(int HWnd, int lParam) {
|
||||||
final pid = pidPointer.value;
|
final structure = Pointer.fromAddress(lParam).cast<Win32Process>();
|
||||||
free(pidPointer);
|
final pidPointer = calloc<Uint32>();
|
||||||
if (pid != structure.ref.pid) {
|
GetWindowThreadProcessId(HWnd, pidPointer);
|
||||||
return TRUE;
|
final pid = pidPointer.value;
|
||||||
|
if (pid == structure.ref.pid) {
|
||||||
|
final length = structure.ref.HWndLength;
|
||||||
|
final newLength = length + 1;
|
||||||
|
final ptr = malloc.allocate<Uint32>(sizeOf<Uint32>() * newLength);
|
||||||
|
final list = structure.ref.HWnd.asTypedList(length);
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
(ptr + i).value = list[i];
|
||||||
}
|
}
|
||||||
|
ptr[list.length] = HWnd;
|
||||||
structure.ref.HWnd = HWnd;
|
structure.ref.HWndLength = newLength;
|
||||||
return FALSE;
|
free(structure.ref.HWnd);
|
||||||
|
structure.ref.HWnd = ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int getHWndFromPid(int pid) {
|
free(pidPointer);
|
||||||
final result = calloc<_Process>();
|
return TRUE;
|
||||||
result.ref.pid = pid;
|
}
|
||||||
EnumWindows(
|
|
||||||
Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
|
List<int> _getHWnds(int pid) {
|
||||||
final HWnd = result.ref.HWnd;
|
final result = calloc<Win32Process>();
|
||||||
|
result.ref.pid = pid;
|
||||||
|
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
|
||||||
|
final length = result.ref.HWndLength;
|
||||||
|
final HWndsPointer = result.ref.HWnd;
|
||||||
|
if(HWndsPointer == nullptr) {
|
||||||
calloc.free(result);
|
calloc.free(result);
|
||||||
return HWnd;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final HWnds = HWndsPointer.asTypedList(length)
|
||||||
|
.toList(growable: false);
|
||||||
|
calloc.free(result);
|
||||||
|
return HWnds;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VirtualDesktopManager {
|
class VirtualDesktopManager {
|
||||||
@@ -382,10 +400,24 @@ class VirtualDesktopManager {
|
|||||||
|
|
||||||
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
|
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
|
||||||
|
|
||||||
void moveWindowToDesktop(int pid, IVirtualDesktop desktop) {
|
Future<void> moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1)}) async {
|
||||||
final HWnd = _Process.getHWndFromPid(pid);
|
final hWNDs = _getHWnds(pid);
|
||||||
final window = applicationViewCollection.getViewForHWnd(HWnd);
|
if(hWNDs.isEmpty) {
|
||||||
windowManager.moveWindowToDesktop(window, desktop);
|
await Future.delayed(pollTime);
|
||||||
|
await moveWindowToDesktop(pid, desktop, pollTime: pollTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(final hWND in hWNDs) {
|
||||||
|
final window = applicationViewCollection.getViewForHWnd(hWND);
|
||||||
|
if(window != null) {
|
||||||
|
windowManager.moveWindowToDesktop(window, desktop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Future.delayed(pollTime);
|
||||||
|
await moveWindowToDesktop(pid, desktop, pollTime: pollTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
IVirtualDesktop createDesktop() => windowManager.createDesktop();
|
IVirtualDesktop createDesktop() => windowManager.createDesktop();
|
||||||
|
|||||||
@@ -256,12 +256,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildSelectorType(),
|
|
||||||
|
|
||||||
const SizedBox(
|
|
||||||
height: 16.0
|
|
||||||
),
|
|
||||||
|
|
||||||
_buildSelector(),
|
_buildSelector(),
|
||||||
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@@ -291,24 +285,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSelectorType() => InfoLabel(
|
|
||||||
label: translations.source,
|
|
||||||
child: Obx(() => ComboBox<FortniteBuildSource>(
|
|
||||||
placeholder: Text(translations.selectBuild),
|
|
||||||
isExpanded: true,
|
|
||||||
items: _buildSources,
|
|
||||||
value: _buildController.selectedBuildSource,
|
|
||||||
onChanged: (value) {
|
|
||||||
if(value == null){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildController.selectedBuildSource = value;
|
|
||||||
_updateFormDefaults();
|
|
||||||
}
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildSelector() => InfoLabel(
|
Widget _buildSelector() => InfoLabel(
|
||||||
label: translations.build,
|
label: translations.build,
|
||||||
child: Obx(() => ComboBox<FortniteBuild>(
|
child: Obx(() => ComboBox<FortniteBuild>(
|
||||||
@@ -328,7 +304,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
List<ComboBoxItem<FortniteBuild>> get _builds => _buildController.builds!
|
List<ComboBoxItem<FortniteBuild>> get _builds => _buildController.builds!
|
||||||
.where((element) => element.source == _buildController.selectedBuild?.source)
|
|
||||||
.map((element) => _buildItem(element))
|
.map((element) => _buildItem(element))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -337,24 +312,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
|||||||
child: Text(element.version.toString())
|
child: Text(element.version.toString())
|
||||||
);
|
);
|
||||||
|
|
||||||
List<ComboBoxItem<FortniteBuildSource>> get _buildSources => FortniteBuildSource.values
|
|
||||||
.map((element) => _buildSourceItem(element))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
ComboBoxItem<FortniteBuildSource> _buildSourceItem(FortniteBuildSource element) => ComboBoxItem<FortniteBuildSource>(
|
|
||||||
value: element,
|
|
||||||
child: Text(_getBuildSourceName(element))
|
|
||||||
);
|
|
||||||
|
|
||||||
String _getBuildSourceName(FortniteBuildSource element) {
|
|
||||||
switch(element) {
|
|
||||||
case FortniteBuildSource.archive:
|
|
||||||
return translations.archive;
|
|
||||||
case FortniteBuildSource.manifest:
|
|
||||||
return translations.manifest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DialogButton> get _stopButton => [
|
List<DialogButton> get _stopButton => [
|
||||||
DialogButton(
|
DialogButton(
|
||||||
text: "Stop",
|
text: "Stop",
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import 'package:async/async.dart';
|
|||||||
import 'package:dart_ipify/dart_ipify.dart';
|
import 'package:dart_ipify/dart_ipify.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:local_notifier/local_notifier.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/update_controller.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.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/dialog_button.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||||
@@ -23,7 +24,6 @@ import 'package:reboot_launcher/src/util/log.dart';
|
|||||||
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
import 'package:reboot_launcher/src/util/matchmaker.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
class LaunchButton extends StatefulWidget {
|
class LaunchButton extends StatefulWidget {
|
||||||
final bool host;
|
final bool host;
|
||||||
@@ -37,11 +37,13 @@ class LaunchButton extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LaunchButtonState extends State<LaunchButton> {
|
class _LaunchButtonState extends State<LaunchButton> {
|
||||||
|
static const Duration _kRebootDelay = Duration(seconds: 10);
|
||||||
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final HostingController _hostingController = Get.find<HostingController>();
|
final HostingController _hostingController = Get.find<HostingController>();
|
||||||
final BackendController _backendController = Get.find<BackendController>();
|
final BackendController _backendController = Get.find<BackendController>();
|
||||||
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
final UpdateController _updateController = Get.find<UpdateController>();
|
||||||
InfoBarEntry? _gameClientInfoBar;
|
InfoBarEntry? _gameClientInfoBar;
|
||||||
InfoBarEntry? _gameServerInfoBar;
|
InfoBarEntry? _gameServerInfoBar;
|
||||||
CancelableOperation? _operation;
|
CancelableOperation? _operation;
|
||||||
@@ -129,18 +131,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log("[${widget.host ? 'HOST' : 'GAME'}] Backend works");
|
log("[${widget.host ? 'HOST' : 'GAME'}] Backend works");
|
||||||
|
|
||||||
log("[${widget.host ? 'HOST' : 'GAME'}] Checking matchmaker(port: ${_matchmakerController.type.value.name}, type: ${_matchmakerController.type.value.name})...");
|
|
||||||
final matchmakerResult = _matchmakerController.started() || await _matchmakerController.toggleInteractive();
|
|
||||||
if(!matchmakerResult){
|
|
||||||
log("[${widget.host ? 'HOST' : 'GAME'}] Cannot start matchmaker");
|
|
||||||
_onStop(
|
|
||||||
reason: _StopReason.matchmakerError
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log("[${widget.host ? 'HOST' : 'GAME'}] Matchmaker works");
|
|
||||||
|
|
||||||
final headless = !forceGUI && _hostingController.headless.value;
|
final headless = !forceGUI && _hostingController.headless.value;
|
||||||
final virtualDesktop = _hostingController.virtualDesktop.value;
|
final virtualDesktop = _hostingController.virtualDesktop.value;
|
||||||
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
|
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
|
||||||
@@ -170,9 +160,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final matchmakingIp = _matchmakerController.gameServerAddress.text;
|
if(_backendController.type.value != ServerType.embedded || !isLocalHost(_backendController.gameServerAddress.text)) {
|
||||||
if(!isLocalHost(matchmakingIp)) {
|
log("[${widget.host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
|
||||||
log("[${widget.host ? 'HOST' : 'GAME'}] The current IP($matchmakingIp) is not set to localhost");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +256,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
_gameController.password.text,
|
_gameController.password.text,
|
||||||
host,
|
host,
|
||||||
_hostingController.headless.value,
|
_hostingController.headless.value,
|
||||||
_gameController.customLaunchArgs.text
|
""
|
||||||
);
|
);
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Generated game args: $gameArgs");
|
log("[${host ? 'HOST' : 'GAME'}] Generated game args: $gameArgs");
|
||||||
final gameProcess = await startProcess(
|
final gameProcess = await startProcess(
|
||||||
@@ -276,60 +265,37 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
wrapProcess: false,
|
wrapProcess: false,
|
||||||
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
|
||||||
);
|
);
|
||||||
gameProcess.stdOutput.listen((line) => _onGameOutput(line, host, virtualDesktop, false));
|
gameProcess.stdOutput.listen((line) => _onGameOutput(line, version, host, virtualDesktop, false));
|
||||||
gameProcess.stdError.listen((line) => _onGameOutput(line, host, virtualDesktop, true));
|
gameProcess.stdError.listen((line) => _onGameOutput(line, version, host, virtualDesktop, true));
|
||||||
watchProcess(gameProcess.pid).then((_) async {
|
watchProcess(gameProcess.pid).then((_) async {
|
||||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
if(instance == null || !host || !headless || instance.launched) {
|
if(instance == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!host || !headless || instance.launched) {
|
||||||
_onStop(reason: _StopReason.exitCode);
|
_onStop(reason: _StopReason.exitCode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(widget.host) {
|
await _restartGameServer(version, virtualDesktop, _StopReason.exitCode);
|
||||||
await _onStop(reason: _StopReason.exitCode);
|
|
||||||
_toggle(forceGUI: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _onStop(
|
|
||||||
reason: _StopReason.exitCode,
|
|
||||||
host: true
|
|
||||||
);
|
|
||||||
final linkedHostingInstance = await _startMatchMakingServer(version, false, virtualDesktop, true);
|
|
||||||
_gameController.instance.value?.child = linkedHostingInstance;
|
|
||||||
if(linkedHostingInstance != null){
|
|
||||||
_setStarted(true, true);
|
|
||||||
_showLaunchingGameServerWidget();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if(host && !headless && virtualDesktop) {
|
|
||||||
final name = version.name;
|
|
||||||
final pid = gameProcess.pid;
|
|
||||||
_moveProcessToVirtualDesktop(name, pid);
|
|
||||||
}
|
|
||||||
return gameProcess.pid;
|
return gameProcess.pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _moveProcessToVirtualDesktop(String versionName, int pid) async {
|
Future<void> _restartGameServer(FortniteVersion version, bool virtualDesktop, _StopReason reason) async {
|
||||||
try {
|
if (widget.host) {
|
||||||
final windowManager = VirtualDesktopManager.getInstance();
|
await _onStop(reason: reason);
|
||||||
_virtualDesktop = windowManager.createDesktop();
|
_toggle(forceGUI: true);
|
||||||
windowManager.setDesktopName(_virtualDesktop!, "$versionName Server (Reboot Launcher)");
|
} else {
|
||||||
Object? lastError;
|
await _onStop(reason: reason, host: true);
|
||||||
for(var i = 0; i < 10; i++) {
|
final linkedHostingInstance =
|
||||||
try {
|
await _startMatchMakingServer(version, false, virtualDesktop, true);
|
||||||
windowManager.moveWindowToDesktop(pid, _virtualDesktop!);
|
_gameController.instance.value?.child = linkedHostingInstance;
|
||||||
break;
|
if (linkedHostingInstance != null) {
|
||||||
}catch(error) {
|
_setStarted(true, true);
|
||||||
lastError = error;
|
_showLaunchingGameServerWidget();
|
||||||
await Future.delayed(Duration(seconds: 1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if(lastError != null) {
|
|
||||||
log("[VIRTUAL_DESKTOP] Cannot move window: $lastError");
|
|
||||||
}
|
|
||||||
}catch(error) {
|
|
||||||
log("[VIRTUAL_DESKTOP] Virtual desktop is not supported: $error");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +314,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onGameOutput(String line, bool host, bool virtualDesktop, bool error) async {
|
void _onGameOutput(String line, FortniteVersion version, bool host, bool virtualDesktop, bool error) async {
|
||||||
if (line.contains(kShutdownLine)) {
|
if (line.contains(kShutdownLine)) {
|
||||||
_onStop(
|
_onStop(
|
||||||
reason: _StopReason.normal
|
reason: _StopReason.normal
|
||||||
@@ -371,17 +337,74 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
if(kLoggedInLines.every((entry) => line.contains(entry))) {
|
||||||
await _injectOrShowError(_Injectable.memoryFix, host);
|
|
||||||
if(!host){
|
|
||||||
await _injectOrShowError(_Injectable.console, host);
|
|
||||||
_onGameClientInjected();
|
|
||||||
}else {
|
|
||||||
await _injectOrShowError(_Injectable.reboot, host);
|
|
||||||
_onGameServerInjected();
|
|
||||||
}
|
|
||||||
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
|
||||||
instance?.launched = true;
|
if(instance != null && !instance.launched) {
|
||||||
instance?.tokenError = false;
|
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) {
|
||||||
|
final hostingInstance = _hostingController.instance.value;
|
||||||
|
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
|
||||||
|
hostingInstance.movedToVirtualDesktop = true;
|
||||||
|
try {
|
||||||
|
final windowManager = VirtualDesktopManager.getInstance();
|
||||||
|
_virtualDesktop = windowManager.createDesktop();
|
||||||
|
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
|
||||||
|
try {
|
||||||
|
await windowManager.moveWindowToDesktop(hostingInstance.gamePid, _virtualDesktop!);
|
||||||
|
}catch(error) {
|
||||||
|
log("[VIRTUAL_DESKTOP] $error");
|
||||||
|
try {
|
||||||
|
windowManager.removeDesktop(_virtualDesktop!);
|
||||||
|
}catch(error) {
|
||||||
|
log("[VIRTUAL_DESKTOP] $error");
|
||||||
|
}finally {
|
||||||
|
_virtualDesktop = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch(error) {
|
||||||
|
log("[VIRTUAL_DESKTOP] $error");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,12 +427,12 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
duration: null
|
duration: null
|
||||||
);
|
);
|
||||||
final gameServerPort = _settingsController.gameServerPort.text;
|
final gameServerPort = _settingsController.gameServerPort.text;
|
||||||
|
_gameServerInfoBar?.close();
|
||||||
final localPingResult = await pingGameServer(
|
final localPingResult = await pingGameServer(
|
||||||
"127.0.0.1:$gameServerPort",
|
"127.0.0.1:$gameServerPort",
|
||||||
timeout: const Duration(minutes: 2)
|
timeout: const Duration(minutes: 2)
|
||||||
);
|
);
|
||||||
if (!localPingResult) {
|
if (!localPingResult) {
|
||||||
_gameServerInfoBar?.close();
|
|
||||||
showInfoBar(
|
showInfoBar(
|
||||||
translations.gameServerStartWarning,
|
translations.gameServerStartWarning,
|
||||||
severity: InfoBarSeverity.error,
|
severity: InfoBarSeverity.error,
|
||||||
@@ -418,7 +441,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_matchmakerController.joinLocalHost();
|
_backendController.joinLocalHost();
|
||||||
final accessible = await _checkGameServer(theme, gameServerPort);
|
final accessible = await _checkGameServer(theme, gameServerPort);
|
||||||
if (!accessible) {
|
if (!accessible) {
|
||||||
showInfoBar(
|
showInfoBar(
|
||||||
@@ -445,7 +468,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async {
|
||||||
try {
|
try {
|
||||||
_gameServerInfoBar?.close();
|
|
||||||
_gameServerInfoBar = showInfoBar(
|
_gameServerInfoBar = showInfoBar(
|
||||||
translations.checkingGameServer,
|
translations.checkingGameServer,
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -494,11 +516,11 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
await _operation?.cancel();
|
await _operation?.cancel();
|
||||||
_operation = null;
|
_operation = null;
|
||||||
await _backendController.worker?.cancel();
|
await _backendController.worker?.cancel();
|
||||||
await _matchmakerController.worker?.cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
host = host ?? widget.host;
|
host = host ?? widget.host;
|
||||||
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
|
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
|
||||||
|
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
|
||||||
if(host) {
|
if(host) {
|
||||||
_hostingController.discardServer();
|
_hostingController.discardServer();
|
||||||
}
|
}
|
||||||
@@ -526,12 +548,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setStarted(host, false);
|
_setStarted(host, false);
|
||||||
if(!reason.isError) {
|
if(host) {
|
||||||
if(host) {
|
_gameServerInfoBar?.close();
|
||||||
_gameServerInfoBar?.close();
|
}else {
|
||||||
}else {
|
_gameClientInfoBar?.close();
|
||||||
_gameClientInfoBar?.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(reason) {
|
switch(reason) {
|
||||||
@@ -633,7 +653,14 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
String _getDllPath(_Injectable injectable) {
|
String _getDllPath(_Injectable injectable) {
|
||||||
switch(injectable){
|
switch(injectable){
|
||||||
case _Injectable.reboot:
|
case _Injectable.reboot:
|
||||||
return _settingsController.gameServerDll.text;
|
if(_updateController.customGameServer.value) {
|
||||||
|
final file = File(_settingsController.gameServerDll.text);
|
||||||
|
if(file.existsSync()) {
|
||||||
|
return file.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rebootDllFile.path;
|
||||||
case _Injectable.console:
|
case _Injectable.console:
|
||||||
return _settingsController.unrealEngineConsoleDll.text;
|
return _settingsController.unrealEngineConsoleDll.text;
|
||||||
case _Injectable.sslBypassV2:
|
case _Injectable.sslBypassV2:
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:reboot_launcher/src/page/pages.dart';
|
|
||||||
import 'package:skeletons/skeletons.dart';
|
|
||||||
|
|
||||||
class InfoTile extends StatelessWidget {
|
class InfoTile extends StatelessWidget {
|
||||||
final Key? expanderKey;
|
final Key? expanderKey;
|
||||||
|
|||||||
@@ -4,21 +4,18 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
class ServerButton extends StatefulWidget {
|
class ServerButton extends StatefulWidget {
|
||||||
final bool backend;
|
const ServerButton({Key? key}) : super(key: key);
|
||||||
const ServerButton({Key? key, required this.backend}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ServerButton> createState() => _ServerButtonState();
|
State<ServerButton> createState() => _ServerButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ServerButtonState extends State<ServerButton> {
|
class _ServerButtonState extends State<ServerButton> {
|
||||||
late final ServerController _controller = widget.backend ? Get.find<BackendController>() : Get.find<MatchmakerController>();
|
late final BackendController _controller = Get.find<BackendController>();
|
||||||
late final StreamController<void> _textController = StreamController.broadcast();
|
late final StreamController<void> _textController = StreamController.broadcast();
|
||||||
late final void Function() _listener = () => _textController.add(null);
|
late final void Function() _listener = () => _textController.add(null);
|
||||||
|
|
||||||
@@ -55,14 +52,14 @@ class _ServerButtonState extends State<ServerButton> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
String get _buttonText {
|
String get _buttonText {
|
||||||
if(_controller.type.value == ServerType.local && _controller.port.text.trim() == _controller.defaultPort.toString()){
|
if(_controller.type.value == ServerType.local && _controller.port.text.trim() == kDefaultBackendPort.toString()){
|
||||||
return translations.checkServer(_controller.controllerName);
|
return translations.checkServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_controller.started.value){
|
if(_controller.started.value){
|
||||||
return translations.stopServer(_controller.controllerName);
|
return translations.stopServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return translations.startServer(_controller.controllerName);
|
return translations.startServer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ import 'package:fluent_ui/fluent_ui.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
import 'package:reboot_common/common.dart';
|
||||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
|
|
||||||
class ServerTypeSelector extends StatefulWidget {
|
class ServerTypeSelector extends StatefulWidget {
|
||||||
final bool backend;
|
const ServerTypeSelector({Key? key})
|
||||||
|
|
||||||
const ServerTypeSelector({Key? key, required this.backend})
|
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -17,7 +13,7 @@ class ServerTypeSelector extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
class _ServerTypeSelectorState extends State<ServerTypeSelector> {
|
||||||
late final ServerController _controller = widget.backend ? Get.find<BackendController>() : Get.find<MatchmakerController>();
|
late final BackendController _controller = Get.find<BackendController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_common/common.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/widget/title_bar_buttons.dart';
|
import 'package:reboot_launcher/src/widget/title_bar_buttons.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/util/translations.dart';
|
import 'package:reboot_launcher/src/util/translations.dart';
|
||||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||||
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
import 'package:reboot_launcher/src/widget/version_selector.dart';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: reboot_launcher
|
name: reboot_launcher
|
||||||
description: Graphical User Interface for Project Reboot
|
description: Graphical User Interface for Project Reboot
|
||||||
version: "9.0.7"
|
version: "9.0.8"
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
@@ -8,43 +8,69 @@ environment:
|
|||||||
sdk: ">=3.0.0 <=3.3.4"
|
sdk: ">=3.0.0 <=3.3.4"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
# The flutter SDK
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
# Reboot common module
|
||||||
reboot_common:
|
reboot_common:
|
||||||
path: ./../common
|
path: ./../common
|
||||||
|
|
||||||
|
# Windows UI 3
|
||||||
|
fluent_ui: ^4.8.7
|
||||||
flutter_acrylic:
|
flutter_acrylic:
|
||||||
path: ./dependencies/flutter_acrylic
|
path: ./dependencies/flutter_acrylic
|
||||||
fluent_ui: ^4.8.7
|
|
||||||
fluentui_system_icons: ^1.1.238
|
fluentui_system_icons: ^1.1.238
|
||||||
bitsdojo_window: ^0.1.5
|
|
||||||
system_theme: ^2.0.0
|
system_theme: ^2.0.0
|
||||||
file_picker: ^8.0.3
|
|
||||||
url_launcher: ^6.1.5
|
|
||||||
archive: ^3.3.1
|
|
||||||
crypto: ^3.0.2
|
|
||||||
async: ^2.8.2
|
|
||||||
get: ^4.6.5
|
|
||||||
get_storage: ^2.0.3
|
|
||||||
window_manager: ^0.3.8
|
|
||||||
clipboard: ^0.1.3
|
|
||||||
universal_disk_space: ^0.2.3
|
|
||||||
uuid: ^3.0.6
|
|
||||||
supabase_flutter: ^2.5.2
|
|
||||||
skeletons: ^0.0.3
|
skeletons: ^0.0.3
|
||||||
|
|
||||||
|
# Window management
|
||||||
|
bitsdojo_window: ^0.1.5
|
||||||
|
window_manager: ^0.3.8
|
||||||
|
|
||||||
|
# Extract zip archives (for example the reboot.zip coming from github)
|
||||||
|
archive: ^3.3.1
|
||||||
|
|
||||||
|
# Cryptographic functions
|
||||||
|
crypto: ^3.0.2
|
||||||
bcrypt: ^1.1.3
|
bcrypt: ^1.1.3
|
||||||
dart_ipify: ^1.1.1
|
|
||||||
path: ^1.8.3
|
|
||||||
pointycastle: ^3.7.3
|
pointycastle: ^3.7.3
|
||||||
|
|
||||||
|
# Async helpers
|
||||||
|
async: ^2.8.2
|
||||||
sync: ^0.3.0
|
sync: ^0.3.0
|
||||||
process_run: ^0.14.2
|
|
||||||
auto_animated_list: ^1.0.4
|
# State management
|
||||||
|
get: ^4.6.5
|
||||||
|
|
||||||
|
# Native utilities
|
||||||
|
clipboard: ^0.1.3
|
||||||
app_links: ^6.0.2
|
app_links: ^6.0.2
|
||||||
url_protocol: ^1.0.0
|
url_protocol: ^1.0.0
|
||||||
intl: any
|
|
||||||
windows_taskbar: ^1.1.2
|
windows_taskbar: ^1.1.2
|
||||||
|
file_picker: ^8.0.3
|
||||||
|
url_launcher: ^6.1.5
|
||||||
|
local_notifier: ^0.1.6
|
||||||
|
|
||||||
|
# Server browser
|
||||||
|
supabase_flutter: ^2.5.2
|
||||||
|
uuid: ^3.0.6
|
||||||
|
dart_ipify: ^1.1.1
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
get_storage: ^2.0.3
|
||||||
|
universal_disk_space: ^0.2.3
|
||||||
|
path: ^1.8.3
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
intl: any
|
||||||
flutter_localized_locales: ^2.0.5
|
flutter_localized_locales: ^2.0.5
|
||||||
|
|
||||||
|
# Auto updater
|
||||||
|
yaml: ^3.1.2
|
||||||
|
package_info_plus: ^8.0.0
|
||||||
|
version: ^3.0.2
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
xml: ^6.3.0
|
xml: ^6.3.0
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
@@ -72,14 +98,14 @@ flutter:
|
|||||||
- assets/backend/profiles/
|
- assets/backend/profiles/
|
||||||
- assets/backend/public/
|
- assets/backend/public/
|
||||||
- assets/backend/responses/
|
- assets/backend/responses/
|
||||||
- assets/matchmaker/
|
|
||||||
- assets/build/
|
- assets/build/
|
||||||
|
- assets/info/en/
|
||||||
|
|
||||||
msix_config:
|
msix_config:
|
||||||
display_name: Reboot Launcher
|
display_name: Reboot Launcher
|
||||||
publisher_display_name: Auties00
|
publisher_display_name: Auties00
|
||||||
identity_name: 31868Auties00.RebootLauncher
|
identity_name: 31868Auties00.RebootLauncher
|
||||||
msix_version: 9.0.0.7
|
msix_version: 9.0.0.8
|
||||||
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
||||||
logo_path: ./assets/icons/reboot.ico
|
logo_path: ./assets/icons/reboot.ico
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||||
|
#include <local_notifier/local_notifier_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever/screen_retriever_plugin.h>
|
||||||
#include <system_theme/system_theme_plugin.h>
|
#include <system_theme/system_theme_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
@@ -22,6 +23,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||||
FlutterAcrylicPluginRegisterWithRegistrar(
|
FlutterAcrylicPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
|
||||||
|
LocalNotifierPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
|
||||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||||
SystemThemePluginRegisterWithRegistrar(
|
SystemThemePluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
app_links
|
app_links
|
||||||
bitsdojo_window_windows
|
bitsdojo_window_windows
|
||||||
flutter_acrylic
|
flutter_acrylic
|
||||||
|
local_notifier
|
||||||
screen_retriever
|
screen_retriever
|
||||||
system_theme
|
system_theme
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
# https://github.com/nexe/nexe
|
|
||||||
npx nexe --build
|
npx nexe --build
|
||||||
77
lawin/structure/matchmaker.js
Normal file
77
lawin/structure/matchmaker.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const functions = require("./functions.js");
|
||||||
|
|
||||||
|
module.exports = async (ws) => {
|
||||||
|
const ticketId = functions.MakeID().replace(/-/gi, "");
|
||||||
|
const matchId = functions.MakeID().replace(/-/gi, "");
|
||||||
|
const sessionId = functions.MakeID().replace(/-/gi, "");
|
||||||
|
|
||||||
|
Connecting();
|
||||||
|
Waiting();
|
||||||
|
Queued();
|
||||||
|
SessionAssignment();
|
||||||
|
Join();
|
||||||
|
|
||||||
|
function Connecting() {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
payload: {
|
||||||
|
state: "Connecting",
|
||||||
|
},
|
||||||
|
name: "StatusUpdate",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Waiting() {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
payload: {
|
||||||
|
totalPlayers: 1,
|
||||||
|
connectedPlayers: 1,
|
||||||
|
state: "Waiting",
|
||||||
|
},
|
||||||
|
name: "StatusUpdate",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Queued() {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
payload: {
|
||||||
|
ticketId: ticketId,
|
||||||
|
queuedPlayers: 0,
|
||||||
|
estimatedWaitSec: 0,
|
||||||
|
status: {},
|
||||||
|
state: "Queued",
|
||||||
|
},
|
||||||
|
name: "StatusUpdate",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionAssignment() {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
payload: {
|
||||||
|
matchId: matchId,
|
||||||
|
state: "SessionAssignment",
|
||||||
|
},
|
||||||
|
name: "StatusUpdate",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Join() {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
payload: {
|
||||||
|
matchId: matchId,
|
||||||
|
sessionId: sessionId,
|
||||||
|
joinDelaySec: 1,
|
||||||
|
},
|
||||||
|
name: "Play",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ express.get("/fortnite/api/game/v2/matchmakingservice/ticket/player/*", async (r
|
|||||||
res.cookie("currentbuildUniqueId", req.query.bucketId.split(":")[0]);
|
res.cookie("currentbuildUniqueId", req.query.bucketId.split(":")[0]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
"serviceUrl": "ws://127.0.0.1:8080",
|
"serviceUrl": "ws://127.0.0.1:80",
|
||||||
"ticketType": "mms-player",
|
"ticketType": "mms-player",
|
||||||
"payload": "69=",
|
"payload": "69=",
|
||||||
"signature": "420="
|
"signature": "420="
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ const XMLBuilder = require("xmlbuilder");
|
|||||||
const XMLParser = require("xml-parser");
|
const XMLParser = require("xml-parser");
|
||||||
|
|
||||||
const functions = require("./../structure/functions.js");
|
const functions = require("./../structure/functions.js");
|
||||||
|
const matchmaker = require("./matchmaker.js");
|
||||||
|
|
||||||
const port = 80;
|
const port = 80;
|
||||||
|
|
||||||
const wss = new WebSocket({ port: port }, () => console.log("XMPP listening on port", port));
|
const wss = new WebSocket({ port: port }, () => console.log("XMPP and Matchmaker started listening on port", port));
|
||||||
wss.on("error", (err) => {
|
wss.on("error", (err) => {
|
||||||
console.log("XMPP and Matchmaker \x1b[31mFAILED\x1b[0m to start hosting.");
|
console.log("XMPP and Matchmaker \x1b[31mFAILED\x1b[0m to start hosting.");
|
||||||
})
|
})
|
||||||
@@ -16,11 +17,9 @@ global.Clients = [];
|
|||||||
|
|
||||||
wss.on('connection', async (ws) => {
|
wss.on('connection', async (ws) => {
|
||||||
ws.on('error', () => {});
|
ws.on('error', () => {});
|
||||||
|
|
||||||
if (ws.protocol.toLowerCase() != "xmpp") {
|
if (ws.protocol.toLowerCase() != "xmpp") return matchmaker(ws);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountId = "";
|
var accountId = "";
|
||||||
var jid = "";
|
var jid = "";
|
||||||
var id = "";
|
var id = "";
|
||||||
@@ -40,7 +39,7 @@ wss.on('connection', async (ws) => {
|
|||||||
.attribute("id", ID)
|
.attribute("id", ID)
|
||||||
.attribute("version", "1.0")
|
.attribute("version", "1.0")
|
||||||
.attribute("xml:lang", "en").toString())
|
.attribute("xml:lang", "en").toString())
|
||||||
|
|
||||||
if (Authenticated == true) {
|
if (Authenticated == true) {
|
||||||
ws.send(XMLBuilder.create("stream:features").attribute("xmlns:stream", "http://etherx.jabber.org/streams")
|
ws.send(XMLBuilder.create("stream:features").attribute("xmlns:stream", "http://etherx.jabber.org/streams")
|
||||||
.element("ver").attribute("xmlns", "urn:xmpp:features:rosterver").up()
|
.element("ver").attribute("xmlns", "urn:xmpp:features:rosterver").up()
|
||||||
@@ -73,7 +72,7 @@ wss.on('connection', async (ws) => {
|
|||||||
|
|
||||||
if (decodedBase64 && accountId && decodedBase64.length == 3) {
|
if (decodedBase64 && accountId && decodedBase64.length == 3) {
|
||||||
Authenticated = true;
|
Authenticated = true;
|
||||||
|
|
||||||
console.log(`An xmpp client with the account id ${accountId} has logged in.`);
|
console.log(`An xmpp client with the account id ${accountId} has logged in.`);
|
||||||
|
|
||||||
ws.send(XMLBuilder.create("success").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl").toString());
|
ws.send(XMLBuilder.create("success").attribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl").toString());
|
||||||
@@ -273,4 +272,4 @@ function ifJSON(str) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user