<feat: New release>

This commit is contained in:
Alessandro Autiero
2023-09-09 19:37:05 +02:00
parent 485e757e83
commit 64b85e4f6e
32 changed files with 586 additions and 223 deletions

View File

@@ -1,4 +1,5 @@
const String kDefaultPlayerName = "Player";
const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777";
const String shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
const List<String> corruptedBuildErrors = [

View File

@@ -2,6 +2,7 @@ import 'dart:io';
class GameInstance {
final String versionName;
final int gamePid;
final int? launcherPid;
final int? eacPid;
@@ -10,11 +11,11 @@ class GameInstance {
bool tokenError;
bool linkedHosting;
GameInstance(this.gamePid, this.launcherPid, this.eacPid, this.hosting, this.linkedHosting)
GameInstance(this.versionName, this.gamePid, this.launcherPid, this.eacPid, this.hosting, this.linkedHosting)
: tokenError = false,
assert(!linkedHosting || !hosting, "Only a game instance can have a linked hosting server");
GameInstance._fromJson(this.gamePid, this.launcherPid, this.eacPid, this.observerPid,
GameInstance._fromJson(this.versionName, this.gamePid, this.launcherPid, this.eacPid, this.observerPid,
this.hosting, this.tokenError, this.linkedHosting);
static GameInstance? fromJson(Map<String, dynamic>? json) {
@@ -27,13 +28,14 @@ class GameInstance {
return null;
}
var version = json["versionName"];
var launcherPid = json["launcher"];
var eacPid = json["eac"];
var observerPid = json["observer"];
var hosting = json["hosting"];
var tokenError = json["tokenError"];
var linkedHosting = json["linkedHosting"];
return GameInstance._fromJson(gamePid, launcherPid, eacPid, observerPid, hosting, tokenError, linkedHosting);
return GameInstance._fromJson(version, gamePid, launcherPid, eacPid, observerPid, hosting, tokenError, linkedHosting);
}
void kill() {
@@ -50,6 +52,7 @@ class GameInstance {
}
Map<String, dynamic> toJson() => {
'versionName': versionName,
'game': gamePid,
'launcher': launcherPid,
'eac': eacPid,

View File

@@ -4,29 +4,75 @@ import 'dart:io';
import 'package:ini/ini.dart';
import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
final matchmakerDirectory = Directory("${assetsDirectory.path}\\matchmaker");
final matchmakerStartExecutable = File("${matchmakerDirectory.path}\\fortmatchmaker.exe");
final matchmakerKillExecutable = File("${authenticatorDirectory.path}\\kill.bat");
final matchmakerConfigFile = File("${authenticatorDirectory.path}\\Config\\config.ini");
String? _lastIp;
String? _lastPort;
Semaphore _semaphore = Semaphore();
Future<int> startEmbeddedMatchmaker(bool detached) async => startBackgroundProcess(
executable: matchmakerStartExecutable,
window: detached
);
Future<void> writeMatchmakingIp(String text) async {
var file = File("${authenticatorDirectory}\\Config\\config.ini");
if(!file.existsSync()){
Stream<String?> watchMatchmakingIp() async* {
if(!matchmakerConfigFile.existsSync()){
return;
}
var observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify);
yield* observer.where((event) => event.path == matchmakerConfigFile.path).asyncMap((event) async {
try {
var config = Config.fromString(await matchmakerConfigFile.readAsString());
var ip = config.get("GameServer", "ip");
if(ip == null) {
return null;
}
var port = config.get("GameServer", "port");
if(port == null) {
return null;
}
if(_lastIp == ip && _lastPort == port) {
return null;
}
return port == kDefaultGameServerPort ? ip : "$ip:$port";
}finally {
try {
_semaphore.release();
} on StateError catch(_) {
// Intended behaviour
}
}
});
}
Future<void> writeMatchmakingIp(String text) async {
var exists = await matchmakerConfigFile.exists();
if(!exists) {
return;
}
_semaphore.acquire();
var splitIndex = text.indexOf(":");
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
var config = Config.fromString(file.readAsStringSync());
if(port.isBlank) {
port = kDefaultGameServerPort;
}
_lastIp = ip;
_lastPort = port;
var config = Config.fromString(await matchmakerConfigFile.readAsString());
config.set("GameServer", "ip", ip);
config.set("GameServer", "port", port);
file.writeAsStringSync(config.toString());
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
}
Future<bool> isMatchmakerPortFree() async => isPortFree(int.parse(kDefaultMatchmakerPort));
@@ -86,3 +132,18 @@ String? _getHostName(String host) => host.replaceFirst("ws://", "").replaceFirst
String? _getScheme(String host) => host.startsWith("ws://") ? "ws" : host.startsWith("wss://") ? "wss" : null;
extension StringExtension on String {
bool get isBlank {
if(isEmpty) {
return true;
}
for(var char in this.split("")) {
if(char != " ") {
return false;
}
}
return true;
}
}

View File

@@ -15,7 +15,7 @@ dependencies:
archive: ^3.3.7
ini: ^2.1.0
shelf_proxy: ^1.0.2
process_run: ^0.13.1
sync: ^0.3.0
dev_dependencies:
flutter_lints: ^2.0.1

View File

@@ -18,6 +18,7 @@ import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/watch.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:system_theme/system_theme.dart';
@@ -41,6 +42,7 @@ void main() async {
var urlError = await _initUrlHandler();
var windowError = await _initWindow();
var observerError = _initObservers();
_checkGameServer();
runApp(const RebootApplication());
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors([urlError, storageError, windowError, observerError]));
},
@@ -52,6 +54,32 @@ void main() async {
void _handleErrors(List<Object?> errors) => errors.where((element) => element != null).forEach((element) => onError(element, null, false));
Future<void> _checkGameServer() async {
try {
var matchmakerController = Get.find<MatchmakerController>();
var address = matchmakerController.gameServerAddress.text;
if(isLocalHost(address)) {
return;
}
var result = await pingGameServer(address);
if(result) {
return;
}
var oldOwner = matchmakerController.gameServerOwner.value;
matchmakerController.joinLocalHost();
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
"$oldOwner's server is no longer available",
severity: InfoBarSeverity.warning,
duration: snackbarLongDuration
));
}catch(_) {
// Intended behaviour
// Just ignore the error
}
}
Future<Object?> _initUrlHandler() async {
try {
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
@@ -69,12 +97,12 @@ Future<Object?> _initUrlHandler() async {
}
void _joinServer(Uri uri) {
var gameController = Get.find<GameController>();
var hostingController = Get.find<HostingController>();
var matchmakerController = Get.find<MatchmakerController>();
var uuid = _parseCustomUrl(uri);
var server = gameController.findServerById(uuid);
var server = hostingController.findServerById(uuid);
if(server != null) {
matchmakerController.joinServer(server);
matchmakerController.joinServer(hostingController.uuid, server);
}else {
showInfoBar(
"No server found: invalid or expired link",
@@ -133,12 +161,12 @@ Object? _initObservers() {
Future<Object?> _initStorage() async {
try {
await GetStorage("reboot_game", settingsDirectory.path).initStorage;
await GetStorage("reboot_authenticator", settingsDirectory.path).initStorage;
await GetStorage("reboot_matchmaker", settingsDirectory.path).initStorage;
await GetStorage("reboot_update", settingsDirectory.path).initStorage;
await GetStorage("reboot_settings", settingsDirectory.path).initStorage;
await GetStorage("reboot_hosting", settingsDirectory.path).initStorage;
await GetStorage("game", settingsDirectory.path).initStorage;
await GetStorage("authenticator", settingsDirectory.path).initStorage;
await GetStorage("matchmaker", settingsDirectory.path).initStorage;
await GetStorage("update", settingsDirectory.path).initStorage;
await GetStorage("settings", settingsDirectory.path).initStorage;
await GetStorage("hosting", settingsDirectory.path).initStorage;
Get.put(GameController());
Get.put(AuthenticatorController());
Get.put(MatchmakerController());

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
@@ -10,7 +8,7 @@ class AuthenticatorController extends ServerController {
String get controllerName => "authenticator";
@override
String get storageName => "reboot_authenticator";
String get storageName => "authenticator";
@override
String get defaultHost => kDefaultAuthenticatorHost;

View File

@@ -2,16 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:supabase/src/supabase_stream_builder.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
class GameController extends GetxController {
late final String uuid;
late final GetStorage _storage;
late final TextEditingController username;
late final TextEditingController password;
@@ -20,53 +15,39 @@ class GameController extends GetxController {
late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool started;
late final RxBool autoStartGameServer;
late final Rxn<Set<Map<String, dynamic>>> servers;
late final Rxn<GameInstance> instance;
GameController() {
_storage = GetStorage("reboot_game");
Iterable decodedVersionsJson = jsonDecode(_storage.read("versions") ?? "[]");
_storage = GetStorage("game");
Iterable decodedVersionsJson = jsonDecode(
_storage.read("versions") ?? "[]");
var decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry))
.toList();
versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions());
var decodedSelectedVersionName = _storage.read("version");
var decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName);
uuid = _storage.read("uuid") ?? const Uuid().v4();
_storage.write("uuid", uuid);
var decodedSelectedVersion = decodedVersions.firstWhereOrNull((
element) => element.name == decodedSelectedVersionName);
_selectedVersion = Rxn(decodedSelectedVersion);
username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName);
username = TextEditingController(
text: _storage.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage.write("username", username.text));
password = TextEditingController(text: _storage.read("password") ?? "");
password.addListener(() => _storage.write("password", password.text));
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
customLaunchArgs =
TextEditingController(text: _storage.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() =>
_storage.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false);
autoStartGameServer = RxBool(_storage.read("auto_game_server") ?? true);
autoStartGameServer.listen((value) => _storage.write("auto_game_server", value));
var supabase = Supabase.instance.client;
servers = Rxn();
supabase.from('hosts')
.stream(primaryKey: ['id'])
.map((event) => _parseValidServers(event))
.listen((event) {
if(servers.value == null) {
servers.value = event;
}else {
servers.value?.addAll(event);
}
});
autoStartGameServer.listen((value) =>
_storage.write("auto_game_server", value));
var serializedInstance = _storage.read("instance");
instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null);
instance.listen((_) => saveInstance());
}
Set<Map<String, dynamic>> _parseValidServers(SupabaseStreamEvent event) => event.where((element) => _isValidServer(element)).toSet();
bool _isValidServer(Map<String, dynamic> element) =>
(kDebugMode || element["id"] != uuid) && element["ip"] != null;
Future<void> saveInstance() =>
_storage.write("instance", jsonEncode(instance.value?.toJson()));
@@ -123,12 +104,4 @@ class GameController extends GetxController {
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
versions.update((val) => function(version));
}
Map<String, dynamic>? findServerById(String uuid) {
try {
return servers.value?.firstWhere((element) => element["id"] == uuid);
} on StateError catch(_) {
return null;
}
}
}

View File

@@ -4,23 +4,29 @@ 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/util/watch.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
const String kDefaultServerName = "Reboot Game Server";
const String kDefaultDescription = "Just another server";
class HostingController extends GetxController {
late final GetStorage _storage;
late final String uuid;
late final TextEditingController name;
late final TextEditingController description;
late final TextEditingController password;
late final RxBool showPassword;
late final RxBool discoverable;
late final RxBool started;
late final RxBool published;
late final Rxn<GameInstance> instance;
late final Rxn<Set<Map<String, dynamic>>> servers;
HostingController() {
_storage = GetStorage("reboot_hosting");
_storage = GetStorage("hosting");
uuid = _storage.read("uuid") ?? const Uuid().v4();
_storage.write("uuid", uuid);
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
name.addListener(() => _storage.write("name", name.text));
description = TextEditingController(text: _storage.read("description") ?? kDefaultDescription);
@@ -30,11 +36,29 @@ class HostingController extends GetxController {
discoverable = RxBool(_storage.read("discoverable") ?? true);
discoverable.listen((value) => _storage.write("discoverable", value));
started = RxBool(false);
published = RxBool(false);
showPassword = RxBool(false);
var serializedInstance = _storage.read("instance");
instance = Rxn(serializedInstance != null ? GameInstance.fromJson(jsonDecode(serializedInstance)) : null);
instance.listen((_) => saveInstance());
var supabase = Supabase.instance.client;
servers = Rxn();
supabase.from('hosts')
.stream(primaryKey: ['id'])
.map((event) => _parseValidServers(event))
.listen((event) {
if(servers.value == null) {
servers.value = event;
}else {
servers.value?.addAll(event);
}
});
}
Set<Map<String, dynamic>> _parseValidServers(event) => event.where((element) => _isValidServer(element)).toSet();
bool _isValidServer(Map<String, dynamic> element) =>
element["id"] != uuid && element["ip"] != null;
Future<void> saveInstance() => _storage.write("instance", jsonEncode(instance.value?.toJson()));
@@ -46,4 +70,12 @@ class HostingController extends GetxController {
started.value = false;
instance.value = null;
}
Map<String, dynamic>? findServerById(String uuid) {
try {
return servers.value?.firstWhere((element) => element["id"] == uuid);
} on StateError catch(_) {
return null;
}
}
}

View File

@@ -1,24 +1,43 @@
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';
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);
writeMatchmakingIp(gameServerAddress.text);
var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue);
gameServerAddress.addListener(() {
storage.write("game_server_address", gameServerAddress.text);
writeMatchmakingIp(gameServerAddress.text);
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 => "matchmaker";
@override
String get storageName => "reboot_matchmaker";
String get storageName => "matchmaker";
@override
String get defaultHost => kDefaultMatchmakerHost;

View File

@@ -1,8 +1,8 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
class SettingsController extends GetxController {
late final GetStorage _storage;
@@ -19,7 +19,7 @@ class SettingsController extends GetxController {
late double scrollingDistance;
SettingsController() {
_storage = GetStorage("reboot_settings");
_storage = GetStorage("settings");
gameServerDll = _createController("game_server", "reboot.dll");
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
authenticatorDll = _createController("authenticator", "cobalt.dll");
@@ -54,6 +54,7 @@ class SettingsController extends GetxController {
gameServerDll.text = _controllerDefaultPath("reboot.dll");
unrealEngineConsoleDll.text = _controllerDefaultPath("console.dll");
authenticatorDll.text = _controllerDefaultPath("cobalt.dll");
gameServerPort.text = kDefaultGameServerPort;
firstRun.value = true;
}

View File

@@ -12,7 +12,7 @@ class UpdateController {
late final TextEditingController url;
UpdateController() {
_storage = GetStorage("reboot_update");
_storage = GetStorage("update");
timestamp = RxnInt(_storage.read("ts"));
timestamp.listen((value) => _storage.write("ts", value));
var timerIndex = _storage.read("timer");

View File

@@ -1,7 +1,6 @@
import 'dart:collection';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:sync/semaphore.dart';
@@ -18,7 +17,7 @@ void restoreMessage(int lastIndex) {
Overlay.of(pageKey.currentContext!).insert(overlay);
}
void showInfoBar(String text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) {
void showInfoBar(dynamic text, {InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = snackbarShortDuration, Widget? action}) {
try {
_semaphore.acquire();
var index = pageIndex.value;
@@ -29,12 +28,25 @@ void showInfoBar(String text, {InfoBarSeverity severity = InfoBarSeverity.info,
width: double.infinity,
child: Mica(
child: InfoBar(
title: Text(text),
isLong: action == null,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if(text is Widget)
text,
if(text is String)
Text(text),
if(action != null)
action
],
),
isLong: false,
isIconVisible: true,
content: action ?? SizedBox(
content: SizedBox(
width: double.infinity,
child: loading ? const ProgressBar() : const SizedBox()
child: loading ? const Padding(
padding: EdgeInsets.only(top: 8.0, bottom: 2.0),
child: ProgressBar(),
) : const SizedBox()
),
severity: severity
),

View File

@@ -1,7 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
String? lastError;

View File

@@ -1,20 +1,21 @@
import 'dart:async';
import 'package:clipboard/clipboard.dart';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.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_button.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
extension ServerControllerDialog on ServerController {
Future<bool> restartInteractive() async {
@@ -165,7 +166,22 @@ extension ServerControllerDialog on ServerController {
}
extension MatchmakerControllerExtension on MatchmakerController {
Future<void> joinServer(Map<String, dynamic> entry) async {
void joinLocalHost() {
gameServerAddress.text = kDefaultGameServerHost;
gameServerOwner.value = null;
}
Future<void> joinServer(String uuid, Map<String, dynamic> entry) async {
var id = entry["id"];
if(uuid == id) {
showInfoBar(
"You can't join your own server",
duration: snackbarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
var hashedPassword = entry["password"];
var hasPassword = hashedPassword != null;
var embedded = type.value == ServerType.embedded;
@@ -275,6 +291,7 @@ extension MatchmakerControllerExtension on MatchmakerController {
void _onSuccess(bool embedded, String decryptedIp, String author) {
if(embedded) {
gameServerAddress.text = decryptedIp;
gameServerOwner.value = author;
pageIndex.value = 0;
}else {
FlutterClipboard.controlC(decryptedIp);
@@ -286,3 +303,43 @@ extension MatchmakerControllerExtension on MatchmakerController {
));
}
}
extension HostingControllerExtension on HostingController {
Future<void> publishServer(String author, String version) async {
var passwordText = password.text;
var hasPassword = passwordText.isNotEmpty;
var ip = await Ipify.ipv4();
if(hasPassword) {
ip = aes256Encrypt(ip, passwordText);
}
var supabase = Supabase.instance.client;
var hosts = supabase.from('hosts');
var payload = {
'name': name.text,
'description': description.text,
'author': author,
'ip': ip,
'version': version,
'password': hasPassword ? hashPassword(passwordText) : null,
'timestamp': DateTime.now().toIso8601String(),
'discoverable': discoverable.value
};
if(published()) {
await hosts.update(payload).eq("id", uuid);
}else {
payload["id"] = uuid;
await hosts.insert(payload);
}
published.value = true;
}
Future<void> discardServer() async {
var supabase = Supabase.instance.client;
await supabase.from('hosts')
.delete()
.match({'id': uuid});
published.value = false;
}
}

View File

@@ -3,15 +3,13 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/authenticator_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/server/start_button.dart';
import 'package:reboot_launcher/src/widget/server/type_selector.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
class AuthenticatorPage extends StatefulWidget {
const AuthenticatorPage({Key? key}) : super(key: key);
@@ -70,9 +68,19 @@ class _AuthenticatorPageState extends State<AuthenticatorPage> with AutomaticKee
subtitle: "Whether the embedded authenticator should be started as a separate process, useful for debugging",
contentWidth: null,
isChild: true,
content: Obx(() => ToggleSwitch(
content: Obx(() => Row(
children: [
Text(
_authenticatorController.detached.value ? "On" : "Off"
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _authenticatorController.detached(),
onChanged: (value) => _authenticatorController.detached.value = value
),
],
))
),
],

View File

@@ -4,15 +4,12 @@ import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/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/widget/common/setting_tile.dart';
import 'package:skeletons/skeletons.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
class BrowsePage extends StatefulWidget {
const BrowsePage({Key? key}) : super(key: key);
@@ -21,7 +18,7 @@ class BrowsePage extends StatefulWidget {
}
class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMixin {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final MatchmakerController _matchmakerController = Get.find<MatchmakerController>();
final TextEditingController _filterController = TextEditingController();
final StreamController<String> _filterControllerStream = StreamController();
@@ -33,7 +30,10 @@ class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMi
future: Future.delayed(const Duration(seconds: 1)), // Fake delay to show loading
builder: (context, futureSnapshot) => Obx(() {
var ready = futureSnapshot.connectionState == ConnectionState.done;
var data = _gameController.servers.value;
var data = _hostingController.servers
.value
?.where((entry) => entry["discoverable"] ?? false)
.toSet();
if(ready && data?.isEmpty == true) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -108,7 +108,7 @@ class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMi
title: "${_formatName(entry)}${entry["author"]}",
subtitle: "${_formatDescription(entry)}${_formatVersion(entry)}",
content: Button(
onPressed: () => _matchmakerController.joinServer(entry),
onPressed: () => _matchmakerController.joinServer(_hostingController.uuid, entry),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -146,7 +146,7 @@ class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMi
}
bool _isValidItem(Map<String, dynamic> entry, String? filter) =>
(entry["discoverable"] ?? false) && (filter == null || _filterServer(entry, filter));
filter == null || _filterServer(entry, filter);
bool _filterServer(Map<String, dynamic> element, String filter) {
String? id = element["id"];

View File

@@ -4,18 +4,17 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/page/browse_page.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/page/authenticator_page.dart';
import 'package:reboot_launcher/src/page/browse_page.dart';
import 'package:reboot_launcher/src/page/hosting_page.dart';
import 'package:reboot_launcher/src/page/info_page.dart';
import 'package:reboot_launcher/src/page/matchmaker_page.dart';
import 'package:reboot_launcher/src/page/play_page.dart';
import 'package:reboot_launcher/src/page/settings_page.dart';
import 'package:reboot_launcher/src/widget/home/pane.dart';
import 'package:reboot_launcher/src/widget/home/profile.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/widget/os/title_bar.dart';
import 'package:reboot_launcher/src/page/hosting_page.dart';
import 'package:reboot_launcher/src/page/info_page.dart';
import 'package:window_manager/window_manager.dart';
GlobalKey appKey = GlobalKey();
@@ -48,6 +47,13 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
void initState() {
windowManager.addListener(this);
_searchController.addListener(_onSearch);
var lastValue = pageIndex.value;
pageIndex.listen((value) {
if(value != lastValue) {
_pagesStack.add(lastValue);
lastValue = value;
}
});
super.initState();
}
@@ -116,10 +122,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
pane: NavigationPane(
key: appKey,
selected: pageIndex.value,
onChanged: (index) {
_pagesStack.add(pageIndex.value);
pageIndex.value = index;
},
onChanged: (index) => pageIndex.value = index,
menuButton: const SizedBox(),
displayMode: PaneDisplayMode.open,
items: _items,

View File

@@ -1,18 +1,19 @@
import 'package:clipboard/clipboard.dart';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/main.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/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/widget/game/start_button.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
import 'package:sync/semaphore.dart';
class HostingPage extends StatefulWidget {
const HostingPage({Key? key}) : super(key: key);
@@ -24,7 +25,7 @@ class HostingPage extends StatefulWidget {
class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClientMixin {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final UpdateController _updateController = Get.find<UpdateController>();
final Semaphore _semaphore = Semaphore();
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
@override
@@ -48,7 +49,8 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
isChild: true,
content: TextFormBox(
placeholder: "Name",
controller: _hostingController.name
controller: _hostingController.name,
onChanged: (_) => _updateServer()
)
),
SettingTile(
@@ -57,7 +59,8 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
isChild: true,
content: TextFormBox(
placeholder: "Description",
controller: _hostingController.description
controller: _hostingController.description,
onChanged: (_) => _updateServer()
)
),
SettingTile(
@@ -71,7 +74,10 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
obscureText: !_hostingController.showPassword.value,
enableSuggestions: false,
autocorrect: false,
onChanged: (text) => _showPasswordTrailing.value = text.isNotEmpty,
onChanged: (text) {
_showPasswordTrailing.value = text.isNotEmpty;
_updateServer();
},
suffix: Button(
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
style: ButtonStyle(
@@ -90,9 +96,22 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
subtitle: "Make your server available to other players on the server browser",
isChild: true,
contentWidth: null,
content: Obx(() => ToggleSwitch(
content: Obx(() => Row(
children: [
Text(
_hostingController.discoverable.value ? "On" : "Off"
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _hostingController.discoverable(),
onChanged: (value) => _hostingController.discoverable.value = value
onChanged: (value) async {
_hostingController.discoverable.value = value;
await _updateServer();
}
),
],
))
)
],
@@ -138,7 +157,7 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
isChild: true,
content: Button(
onPressed: () async {
FlutterClipboard.controlC("$kCustomUrlSchema://${_gameController.uuid}");
FlutterClipboard.controlC("$kCustomUrlSchema://${_hostingController.uuid}");
showInfoBar(
"Copied your link to the clipboard",
severity: InfoBarSeverity.success
@@ -177,6 +196,35 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
)
)
],
),
const SizedBox(
height: 8.0,
),
SettingTile(
title: "Reset game server",
subtitle: "Resets the game server's settings to their default values",
content: Button(
onPressed: () => showAppDialog(
builder: (context) => InfoDialog(
text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
buttons: [
DialogButton(
type: ButtonType.secondary,
text: "Close",
),
DialogButton(
type: ButtonType.primary,
text: "Reset",
onTap: () {
_hostingController.reset();
Navigator.of(context).pop();
},
)
],
)
),
child: const Text("Reset"),
)
)
],
),
@@ -190,4 +238,26 @@ class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClient
],
);
}
Future<void> _updateServer() async {
if(!_hostingController.published()) {
return;
}
try {
_semaphore.acquire();
_hostingController.publishServer(
_gameController.username.text,
_hostingController.instance.value!.versionName
);
} catch(error) {
showInfoBar(
"An error occurred while updating the game server: $error",
severity: InfoBarSeverity.success,
duration: snackbarLongDuration
);
} finally {
_semaphore.release();
}
}
}

View File

@@ -1,8 +1,9 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/util/tutorial.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
class InfoPage extends StatefulWidget {
const InfoPage({Key? key}) : super(key: key);
@@ -123,6 +124,33 @@ class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin
.typography
.title,
contentWidth: null,
),
const SizedBox(
height: 8.0,
),
SettingTile(
title: 'How can I make my game server accessible for other players?',
subtitle: Text.rich(
TextSpan(
children: [
TextSpan(
text: 'Follow ',
style: FluentTheme.of(context).typography.body
),
TextSpan(
text: 'this tutorial',
mouseCursor: SystemMouseCursors.click,
style: FluentTheme.of(context).typography.body?.copyWith(color: FluentTheme.of(context).accentColor),
recognizer: TapGestureRecognizer()..onTap = openPortTutorial
)
]
)
),
titleStyle: FluentTheme
.of(context)
.typography
.title,
contentWidth: null,
)
],
),

View File

@@ -3,14 +3,12 @@ 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/widget/server/type_selector.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.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/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/server/start_button.dart';
import 'package:reboot_launcher/src/widget/server/type_selector.dart';
import 'package:url_launcher/url_launcher.dart';
class MatchmakerPage extends StatefulWidget {
const MatchmakerPage({Key? key}) : super(key: key);
@@ -71,7 +69,8 @@ class _MatchmakerPageState extends State<MatchmakerPage> with AutomaticKeepAlive
isChild: true,
content: TextFormBox(
placeholder: "Address",
controller: _matchmakerController.gameServerAddress
controller: _matchmakerController.gameServerAddress,
focusNode: _matchmakerController.gameServerAddressFocusNode
)
),
if(_matchmakerController.type.value == ServerType.embedded)
@@ -80,9 +79,19 @@ class _MatchmakerPageState extends State<MatchmakerPage> with AutomaticKeepAlive
subtitle: "Whether the embedded matchmaker should be started as a separate process, useful for debugging",
contentWidth: null,
isChild: true,
content: Obx(() => ToggleSwitch(
content: Obx(() => Row(
children: [
Text(
_matchmakerController.detached.value ? "On" : "Off"
),
const SizedBox(
width: 16.0
),
ToggleSwitch(
checked: _matchmakerController.detached.value,
onChanged: (value) => _matchmakerController.detached.value = value
),
],
)),
)
]

View File

@@ -5,8 +5,8 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/matchmaker_controller.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/widget/game/start_button.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:reboot_launcher/src/widget/game/start_button.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
@@ -71,10 +71,26 @@ class _PlayPageState extends State<PlayPage> {
SettingTile(
title: "Game Server",
subtitle: "Helpful shortcuts to find the server where you want to play",
content: IgnorePointer(
child: Button(
style: ButtonStyle(
backgroundColor: ButtonState.all(FluentTheme.of(context).resources.controlFillColorDefault)
),
onPressed: () {},
child: Obx(() {
var address = _matchmakerController.gameServerAddress.text;
var owner = _matchmakerController.gameServerOwner.value;
return Text(
isLocalHost(address) ? "Your server" : owner != null ? "$owner's server" : address,
textAlign: TextAlign.start
);
})
),
),
expandedContent: [
SettingTile(
title: "Host a server",
subtitle: "Do you want to play with your friends? Host a server for them!",
subtitle: "Do you want to create a game server for yourself or your friends? Host one!",
content: Button(
onPressed: () => pageIndex.value = 1,
child: const Text("Host")
@@ -82,13 +98,25 @@ class _PlayPageState extends State<PlayPage> {
isChild: true
),
SettingTile(
title: "Join a server",
subtitle: "Find a server where you can play on the launcher's server browser",
title: "Join a Reboot server",
subtitle: "Find a discoverable server hosted on the Reboot Launcher in the server browser",
content: Button(
onPressed: () => pageIndex.value = 2,
child: const Text("Browse")
),
isChild: true
),
SettingTile(
title: "Join a custom server",
subtitle: "Type the address of any server, whether it was hosted on the Reboot Launcher or not",
content: Button(
onPressed: () {
pageIndex.value = 4;
WidgetsBinding.instance.addPostFrameCallback((_) => _matchmakerController.gameServerAddressFocusNode.requestFocus());
},
child: const Text("Join")
),
isChild: true
)
]
),

View File

@@ -2,19 +2,16 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/authenticator_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_button.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/common/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({Key? key}) : super(key: key);
@@ -24,10 +21,7 @@ class SettingsPage extends StatefulWidget {
}
class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClientMixin {
final BuildController _buildController = Get.find<BuildController>();
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final AuthenticatorController _authenticatorController = Get.find<AuthenticatorController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>();
@@ -108,6 +102,7 @@ class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClie
text: Text(entry.text),
onPressed: () {
_updateController.timer.value = entry;
removeMessage(6);
_updateController.update(true);
}
)).toList()
@@ -148,7 +143,7 @@ class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClie
content: Button(
onPressed: () => showAppDialog(
builder: (context) => InfoDialog(
text: "Do you want to reset all the launcher's settings to their default values? This action is irreversible",
text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
buttons: [
DialogButton(
type: ButtonType.secondary,
@@ -158,12 +153,7 @@ class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClie
type: ButtonType.primary,
text: "Reset",
onTap: () {
_buildController.reset();
_gameController.reset();
_hostingController.reset();
_authenticatorController.reset();
_settingsController.reset();
_updateController.reset();
Navigator.of(context).pop();
},
)

View File

@@ -1,8 +1,9 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:bcrypt/bcrypt.dart';
import 'package:pointycastle/export.dart';
import 'dart:convert';
const int _ivLength = 16;
const int _keyLength = 32;

View File

@@ -0,0 +1,3 @@
import 'package:url_launcher/url_launcher.dart';
Future<void> openPortTutorial() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/blob/master/documentation/PortForwarding.md"));

View File

@@ -28,7 +28,7 @@ extension GameInstanceWatcher on GameInstance {
observerPid = await startBackgroundProcess(
executable: _executable,
args: [
_gameController.uuid,
_hostingController.uuid,
gamePid.toString(),
launcherPid?.toString() ?? "-1",
eacPid?.toString() ?? "-1",
@@ -50,6 +50,6 @@ extension GameInstanceWatcher on GameInstance {
_hostingController.instance.value?.kill();
await _supabase.from('hosts')
.delete()
.match({'id': _gameController.uuid});
.match({'id': _hostingController.uuid});
}
}

View File

@@ -63,13 +63,19 @@ class _FileSelectorState extends State<FileSelector> {
_selecting = true;
if(widget.folder) {
compute(openFolderPicker, widget.windowTitle)
.then((value) => widget.controller.text = value ?? widget.controller.text)
.then(_updateText)
.then((_) => _selecting = false);
return;
}
compute(openFilePicker, widget.extension!)
.then((value) => widget.controller.text = value ?? widget.controller.text)
.then(_updateText)
.then((_) => _selecting = false);
}
void _updateText(String? value) {
var text = value ?? widget.controller.text;
widget.controller.text = value ?? widget.controller.text;
widget.controller.selection = TextSelection.collapsed(offset: text.length);
}
}

View File

@@ -8,7 +8,7 @@ class SettingTile extends StatefulWidget {
final String? title;
final TextStyle? titleStyle;
final String? subtitle;
final dynamic subtitle;
final TextStyle? subtitleStyle;
final Widget? content;
final double? contentWidth;
@@ -27,10 +27,9 @@ class SettingTile extends StatefulWidget {
this.expandedContentHeaderHeight = kDefaultHeaderHeight,
this.expandedContent,
this.isChild = false})
: assert(
(title == null && subtitle == null) ||
(title != null && subtitle != null),
"Title and subtitle can only be null together"),
: assert((title == null && subtitle == null) || (title != null && subtitle != null), "title and subtitle can only be null together"),
assert(subtitle == null || subtitle is String || subtitle is Widget, "subtitle can only be null, String or Widget"),
assert(subtitle is! Widget || subtitleStyle == null, "subtitleStyle must be null if subtitle is a widget"),
super(key: key);
@override
@@ -112,7 +111,7 @@ class _SettingTileState extends State<SettingTile> {
),
);
Widget get _subtitle => Text(
Widget get _subtitle => widget.subtitle is Widget ? widget.subtitle : Text(
widget.subtitle!,
style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body
);

View File

@@ -1,25 +1,26 @@
import 'dart:async';
import 'dart:io';
import 'package:async/async.dart';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import 'package:process_run/shell.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/authenticator_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/authenticator_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/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/game.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/tutorial.dart';
import 'package:reboot_launcher/src/util/watch.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:url_launcher/url_launcher.dart';
class LaunchButton extends StatefulWidget {
final bool host;
@@ -131,7 +132,7 @@ class _LaunchButtonState extends State<LaunchButton> {
var eacProcess = await _createEacProcess(version);
var executable = await version.executable;
var gameProcess = await _createGameProcess(executable!.path, host);
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, host, linkedHosting);
var instance = GameInstance(version.name, gameProcess, launcherProcess, eacProcess, host, linkedHosting);
instance.startObserver();
if(host){
_removeHostEntry();
@@ -149,8 +150,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return false;
}
// var matchmakingIp = _settingsController.matchmakingIp.text;
var matchmakingIp = "127.0.0.1";
var matchmakingIp = _matchmakerController.gameServerAddress.text;
if(!isLocalHost(matchmakingIp)) {
return false;
}
@@ -241,8 +241,9 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
var theme = FluentTheme.of(context);
showInfoBar(
"Waiting for the game server to become available...",
"Waiting for the game server to boot up...",
loading: true,
duration: null
);
@@ -253,54 +254,84 @@ class _LaunchButtonState extends State<LaunchButton> {
);
if(!localPingResult) {
showInfoBar(
"The headless server was started successfully, but the game server isn't available",
"The headless server was started successfully, but the game server didn't boot",
severity: InfoBarSeverity.error,
duration: snackbarLongDuration
);
return;
}
_matchmakerController.joinLocalHost();
var accessible = await _checkAccessible(theme, gameServerPort);
if(!accessible) {
showInfoBar(
"Checking if the game server is accessible...",
loading: true,
duration: null
);
var publicIp = await Ipify.ipv4();
var result = await pingGameServer("$publicIp:$gameServerPort");
if(!result) {
showInfoBar(
"The game server was started successfully, but nobody outside your network can join",
"The game server was started successfully, but other players can't join",
severity: InfoBarSeverity.warning,
duration: null,
action: Button(
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/documentation/PortForwarding.md")),
child: Text("Open port $gameServerPort")
)
duration: snackbarLongDuration
);
return;
}
if(_hostingController.discoverable.value){
var password = _hostingController.password.text;
var hasPassword = password.isNotEmpty;
var ip = await Ipify.ipv4();
if(hasPassword) {
ip = aes256Encrypt(ip, password);
await _hostingController.publishServer(
_gameController.username.text,
_hostingController.instance.value!.versionName,
);
showInfoBar(
"The game server was started successfully",
severity: InfoBarSeverity.success,
duration: snackbarLongDuration
);
}
var supabase = Supabase.instance.client;
await supabase.from('hosts').insert({
'id': _gameController.uuid,
'name': _hostingController.name.text,
'description': _hostingController.description.text,
'author': _gameController.username.text,
'ip': ip,
'version': _gameController.selectedVersion?.name,
'password': hasPassword ? hashPassword(password) : null,
'timestamp': DateTime.now().toIso8601String(),
'discoverable': _hostingController.discoverable.value
});
Future<bool> _checkAccessible(FluentThemeData theme, String gameServerPort) async {
showInfoBar(
"Checking if other players can join the game server...",
loading: true,
duration: null
);
var publicIp = await Ipify.ipv4();
var externalResult = await pingGameServer("$publicIp:$gameServerPort");
if(externalResult) {
return true;
}
var future = CancelableOperation.fromFuture(pingGameServer(
"$publicIp:$gameServerPort",
timeout: const Duration(days: 365)
));
showInfoBar(
Text.rich(
TextSpan(
children: [
const TextSpan(
text: "Other players can't join the game server currently: please follow "
),
TextSpan(
text: "this tutorial",
mouseCursor: SystemMouseCursors.click,
style: TextStyle(
color: theme.accentColor.dark
),
recognizer: TapGestureRecognizer()..onTap = openPortTutorial
),
const TextSpan(
text: " to fix this problem"
),
]
)
),
action: Button(
onPressed: () {
future.cancel();
removeMessage(1);
},
child: const Text("Ignore"),
),
severity: InfoBarSeverity.warning,
duration: null,
loading: true
);
return await future.valueOrCancellation() ?? false;
}
void _onGameOutput(String line, bool host) {
@@ -387,7 +418,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<void> _removeHostEntry() async {
await _supabase.from('hosts')
.delete()
.match({'id': _gameController.uuid});
.match({'id': _hostingController.uuid});
}
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {

View File

@@ -1,7 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/implementation/profile.dart';

View File

@@ -2,15 +2,14 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/version/version_name_input.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:path/path.dart' as path;
class AddLocalVersion extends StatefulWidget {
const AddLocalVersion({Key? key})
@@ -31,7 +30,9 @@ class _AddLocalVersionState extends State<AddLocalVersion> {
var file = Directory(_gamePathController.text);
if(await file.exists()) {
if(_nameController.text.isEmpty) {
_nameController.text = path.basename(_gamePathController.text);
var text = path.basename(_gamePathController.text);
_nameController.text = text;
_nameController.selection = TextSelection.collapsed(offset: text.length);
}
}
});

View File

@@ -5,15 +5,15 @@ import 'dart:isolate';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/version/version_build_selector.dart';
import 'package:reboot_launcher/src/widget/version/version_name_input.dart';
import 'package:universal_disk_space/universal_disk_space.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import '../../dialog/abstract/dialog.dart';
import '../../dialog/abstract/dialog_button.dart';
@@ -334,8 +334,12 @@ class _AddServerVersionState extends State<AddServerVersion> {
return;
}
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
_nameController.text = build.version.toString();
var pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}";
_pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
var buildName = build.version.toString();
_nameController.text = buildName;
_nameController.selection = TextSelection.collapsed(offset: buildName.length);
_formKey.currentState?.validate();
}
}

View File

@@ -6,15 +6,14 @@ import 'package:flutter/gestures.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/widget/version/add_local_version.dart';
import 'package:reboot_launcher/src/widget/version/add_server_version.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reboot_launcher/src/widget/common/file_selector.dart';
import 'package:reboot_launcher/src/widget/version/add_local_version.dart';
import 'package:reboot_launcher/src/widget/version/add_server_version.dart';
import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key);