From 64b85e4f6e34bea6c7cb303b31ff984905745bb6 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Sat, 9 Sep 2023 19:37:05 +0200 Subject: [PATCH] --- common/lib/src/constant/game.dart | 1 + common/lib/src/model/game_instance.dart | 9 +- common/lib/src/util/matchmaker.dart | 71 ++++++++++- common/pubspec.yaml | 2 +- gui/lib/main.dart | 46 +++++-- .../controller/authenticator_controller.dart | 4 +- gui/lib/src/controller/game_controller.dart | 53 ++------ .../src/controller/hosting_controller.dart | 36 +++++- .../src/controller/matchmaker_controller.dart | 27 +++- .../src/controller/settings_controller.dart | 5 +- gui/lib/src/controller/update_controller.dart | 2 +- gui/lib/src/dialog/abstract/info_bar.dart | 24 +++- gui/lib/src/dialog/implementation/error.dart | 3 +- gui/lib/src/dialog/implementation/server.dart | 67 +++++++++- gui/lib/src/page/authenticator_page.dart | 24 ++-- gui/lib/src/page/browse_page.dart | 16 +-- gui/lib/src/page/home_page.dart | 21 ++-- gui/lib/src/page/hosting_page.dart | 96 ++++++++++++-- gui/lib/src/page/info_page.dart | 32 ++++- gui/lib/src/page/matchmaker_page.dart | 27 ++-- gui/lib/src/page/play_page.dart | 36 +++++- gui/lib/src/page/settings_page.dart | 24 ++-- gui/lib/src/util/cryptography.dart | 3 +- gui/lib/src/util/tutorial.dart | 3 + gui/lib/src/util/watch.dart | 4 +- gui/lib/src/widget/common/file_selector.dart | 10 +- gui/lib/src/widget/common/setting_tile.dart | 11 +- gui/lib/src/widget/game/start_button.dart | 117 +++++++++++------- gui/lib/src/widget/home/profile.dart | 1 - .../src/widget/version/add_local_version.dart | 11 +- .../widget/version/add_server_version.dart | 16 ++- .../src/widget/version/version_selector.dart | 7 +- 32 files changed, 586 insertions(+), 223 deletions(-) create mode 100644 gui/lib/src/util/tutorial.dart diff --git a/common/lib/src/constant/game.dart b/common/lib/src/constant/game.dart index 48cad8a..0d1e8a3 100644 --- a/common/lib/src/constant/game.dart +++ b/common/lib/src/constant/game.dart @@ -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 corruptedBuildErrors = [ diff --git a/common/lib/src/model/game_instance.dart b/common/lib/src/model/game_instance.dart index b02d7f4..5d42bcb 100644 --- a/common/lib/src/model/game_instance.dart +++ b/common/lib/src/model/game_instance.dart @@ -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? 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 toJson() => { + 'versionName': versionName, 'game': gamePid, 'launcher': launcherPid, 'eac': eacPid, diff --git a/common/lib/src/util/matchmaker.dart b/common/lib/src/util/matchmaker.dart index 5ec0a8e..69a855c 100644 --- a/common/lib/src/util/matchmaker.dart +++ b/common/lib/src/util/matchmaker.dart @@ -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 startEmbeddedMatchmaker(bool detached) async => startBackgroundProcess( executable: matchmakerStartExecutable, window: detached ); -Future writeMatchmakingIp(String text) async { - var file = File("${authenticatorDirectory}\\Config\\config.ini"); - if(!file.existsSync()){ +Stream 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 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 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; + } +} \ No newline at end of file diff --git a/common/pubspec.yaml b/common/pubspec.yaml index 7fdda5f..e7f0aa9 100644 --- a/common/pubspec.yaml +++ b/common/pubspec.yaml @@ -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 \ No newline at end of file diff --git a/gui/lib/main.dart b/gui/lib/main.dart index 372b4e6..2c6841e 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -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 errors) => errors.where((element) => element != null).forEach((element) => onError(element, null, false)); +Future _checkGameServer() async { + try { + var matchmakerController = Get.find(); + 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 _initUrlHandler() async { try { registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']); @@ -69,12 +97,12 @@ Future _initUrlHandler() async { } void _joinServer(Uri uri) { - var gameController = Get.find(); + var hostingController = Get.find(); var matchmakerController = Get.find(); 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 _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()); diff --git a/gui/lib/src/controller/authenticator_controller.dart b/gui/lib/src/controller/authenticator_controller.dart index eb9a5f6..6a3f860 100644 --- a/gui/lib/src/controller/authenticator_controller.dart +++ b/gui/lib/src/controller/authenticator_controller.dart @@ -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; diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index dd0fb91..e1e7c92 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -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 _selectedVersion; late final RxBool started; late final RxBool autoStartGameServer; - late final Rxn>> servers; late final Rxn 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> _parseValidServers(SupabaseStreamEvent event) => event.where((element) => _isValidServer(element)).toSet(); - - bool _isValidServer(Map element) => - (kDebugMode || element["id"] != uuid) && element["ip"] != null; - Future 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? findServerById(String uuid) { - try { - return servers.value?.firstWhere((element) => element["id"] == uuid); - } on StateError catch(_) { - return null; - } - } } diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index a1deedb..f13b00a 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -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 instance; + late final Rxn>> 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,12 +36,30 @@ 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> _parseValidServers(event) => event.where((element) => _isValidServer(element)).toSet(); + + bool _isValidServer(Map element) => + element["id"] != uuid && element["ip"] != null; + Future saveInstance() => _storage.write("instance", jsonEncode(instance.value?.toJson())); void reset() { @@ -46,4 +70,12 @@ class HostingController extends GetxController { started.value = false; instance.value = null; } + + Map? findServerById(String uuid) { + try { + return servers.value?.firstWhere((element) => element["id"] == uuid); + } on StateError catch(_) { + return null; + } + } } diff --git a/gui/lib/src/controller/matchmaker_controller.dart b/gui/lib/src/controller/matchmaker_controller.dart index a2f6341..5fb233c 100644 --- a/gui/lib/src/controller/matchmaker_controller.dart +++ b/gui/lib/src/controller/matchmaker_controller.dart @@ -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; diff --git a/gui/lib/src/controller/settings_controller.dart b/gui/lib/src/controller/settings_controller.dart index cb56f8f..21ef8ab 100644 --- a/gui/lib/src/controller/settings_controller.dart +++ b/gui/lib/src/controller/settings_controller.dart @@ -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; } diff --git a/gui/lib/src/controller/update_controller.dart b/gui/lib/src/controller/update_controller.dart index c049eb3..e32ced3 100644 --- a/gui/lib/src/controller/update_controller.dart +++ b/gui/lib/src/controller/update_controller.dart @@ -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"); diff --git a/gui/lib/src/dialog/abstract/info_bar.dart b/gui/lib/src/dialog/abstract/info_bar.dart index a3094aa..e2778ea 100644 --- a/gui/lib/src/dialog/abstract/info_bar.dart +++ b/gui/lib/src/dialog/abstract/info_bar.dart @@ -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 ), diff --git a/gui/lib/src/dialog/implementation/error.dart b/gui/lib/src/dialog/implementation/error.dart index 668b3c7..65ad8ec 100644 --- a/gui/lib/src/dialog/implementation/error.dart +++ b/gui/lib/src/dialog/implementation/error.dart @@ -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; diff --git a/gui/lib/src/dialog/implementation/server.dart b/gui/lib/src/dialog/implementation/server.dart index 0679b93..983124c 100644 --- a/gui/lib/src/dialog/implementation/server.dart +++ b/gui/lib/src/dialog/implementation/server.dart @@ -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 restartInteractive() async { @@ -165,7 +166,22 @@ extension ServerControllerDialog on ServerController { } extension MatchmakerControllerExtension on MatchmakerController { - Future joinServer(Map entry) async { + void joinLocalHost() { + gameServerAddress.text = kDefaultGameServerHost; + gameServerOwner.value = null; + } + + Future joinServer(String uuid, Map 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); @@ -285,4 +302,44 @@ extension MatchmakerControllerExtension on MatchmakerController { severity: InfoBarSeverity.success )); } +} + +extension HostingControllerExtension on HostingController { + Future 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 discardServer() async { + var supabase = Supabase.instance.client; + await supabase.from('hosts') + .delete() + .match({'id': uuid}); + published.value = false; + } } \ No newline at end of file diff --git a/gui/lib/src/page/authenticator_page.dart b/gui/lib/src/page/authenticator_page.dart index b9a6227..1f827ca 100644 --- a/gui/lib/src/page/authenticator_page.dart +++ b/gui/lib/src/page/authenticator_page.dart @@ -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 with AutomaticKee subtitle: "Whether the embedded authenticator should be started as a separate process, useful for debugging", contentWidth: null, isChild: true, - content: Obx(() => ToggleSwitch( - checked: _authenticatorController.detached(), - onChanged: (value) => _authenticatorController.detached.value = value + 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 + ), + ], )) ), ], diff --git a/gui/lib/src/page/browse_page.dart b/gui/lib/src/page/browse_page.dart index 40feb2a..92e1531 100644 --- a/gui/lib/src/page/browse_page.dart +++ b/gui/lib/src/page/browse_page.dart @@ -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 with AutomaticKeepAliveClientMixin { - final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); final MatchmakerController _matchmakerController = Get.find(); final TextEditingController _filterController = TextEditingController(); final StreamController _filterControllerStream = StreamController(); @@ -33,7 +30,10 @@ class _BrowsePageState extends State 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 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 with AutomaticKeepAliveClientMi } bool _isValidItem(Map entry, String? filter) => - (entry["discoverable"] ?? false) && (filter == null || _filterServer(entry, filter)); + filter == null || _filterServer(entry, filter); bool _filterServer(Map element, String filter) { String? id = element["id"]; diff --git a/gui/lib/src/page/home_page.dart b/gui/lib/src/page/home_page.dart index e2c648b..e4e1e63 100644 --- a/gui/lib/src/page/home_page.dart +++ b/gui/lib/src/page/home_page.dart @@ -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 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 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, diff --git a/gui/lib/src/page/hosting_page.dart b/gui/lib/src/page/hosting_page.dart index 018fcc7..5783924 100644 --- a/gui/lib/src/page/hosting_page.dart +++ b/gui/lib/src/page/hosting_page.dart @@ -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 with AutomaticKeepAliveClientMixin { final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); - final UpdateController _updateController = Get.find(); + final Semaphore _semaphore = Semaphore(); late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); @override @@ -48,7 +49,8 @@ class _HostingPageState extends State with AutomaticKeepAliveClient isChild: true, content: TextFormBox( placeholder: "Name", - controller: _hostingController.name + controller: _hostingController.name, + onChanged: (_) => _updateServer() ) ), SettingTile( @@ -56,8 +58,9 @@ class _HostingPageState extends State with AutomaticKeepAliveClient subtitle: "The description of your game server", isChild: true, content: TextFormBox( - placeholder: "Description", - controller: _hostingController.description + placeholder: "Description", + controller: _hostingController.description, + onChanged: (_) => _updateServer() ) ), SettingTile( @@ -71,7 +74,10 @@ class _HostingPageState extends State 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 with AutomaticKeepAliveClient subtitle: "Make your server available to other players on the server browser", isChild: true, contentWidth: null, - content: Obx(() => ToggleSwitch( - checked: _hostingController.discoverable(), - onChanged: (value) => _hostingController.discoverable.value = value + content: Obx(() => Row( + children: [ + Text( + _hostingController.discoverable.value ? "On" : "Off" + ), + const SizedBox( + width: 16.0 + ), + ToggleSwitch( + checked: _hostingController.discoverable(), + onChanged: (value) async { + _hostingController.discoverable.value = value; + await _updateServer(); + } + ), + ], )) ) ], @@ -138,7 +157,7 @@ class _HostingPageState extends State 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 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 with AutomaticKeepAliveClient ], ); } + + Future _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(); + } + } } \ No newline at end of file diff --git a/gui/lib/src/page/info_page.dart b/gui/lib/src/page/info_page.dart index 011c8f4..71e8252 100644 --- a/gui/lib/src/page/info_page.dart +++ b/gui/lib/src/page/info_page.dart @@ -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 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, ) ], ), diff --git a/gui/lib/src/page/matchmaker_page.dart b/gui/lib/src/page/matchmaker_page.dart index 0e93683..7baec41 100644 --- a/gui/lib/src/page/matchmaker_page.dart +++ b/gui/lib/src/page/matchmaker_page.dart @@ -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 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 with AutomaticKeepAlive subtitle: "Whether the embedded matchmaker should be started as a separate process, useful for debugging", contentWidth: null, isChild: true, - content: Obx(() => ToggleSwitch( - checked: _matchmakerController.detached.value, - onChanged: (value) => _matchmakerController.detached.value = value + 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 + ), + ], )), ) ] diff --git a/gui/lib/src/page/play_page.dart b/gui/lib/src/page/play_page.dart index 082ec46..02ec91d 100644 --- a/gui/lib/src/page/play_page.dart +++ b/gui/lib/src/page/play_page.dart @@ -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 { 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 { 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 ) ] ), diff --git a/gui/lib/src/page/settings_page.dart b/gui/lib/src/page/settings_page.dart index 8638b06..dc0cf3f 100644 --- a/gui/lib/src/page/settings_page.dart +++ b/gui/lib/src/page/settings_page.dart @@ -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 with AutomaticKeepAliveClientMixin { - final BuildController _buildController = Get.find(); final GameController _gameController = Get.find(); - final HostingController _hostingController = Get.find(); - final AuthenticatorController _authenticatorController = Get.find(); final SettingsController _settingsController = Get.find(); final UpdateController _updateController = Get.find(); @@ -108,6 +102,7 @@ class _SettingsPageState extends State 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 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 with AutomaticKeepAliveClie type: ButtonType.primary, text: "Reset", onTap: () { - _buildController.reset(); - _gameController.reset(); - _hostingController.reset(); - _authenticatorController.reset(); _settingsController.reset(); - _updateController.reset(); Navigator.of(context).pop(); }, ) diff --git a/gui/lib/src/util/cryptography.dart b/gui/lib/src/util/cryptography.dart index 7aa0bec..e3fdb94 100644 --- a/gui/lib/src/util/cryptography.dart +++ b/gui/lib/src/util/cryptography.dart @@ -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; diff --git a/gui/lib/src/util/tutorial.dart b/gui/lib/src/util/tutorial.dart new file mode 100644 index 0000000..37f9977 --- /dev/null +++ b/gui/lib/src/util/tutorial.dart @@ -0,0 +1,3 @@ +import 'package:url_launcher/url_launcher.dart'; + +Future openPortTutorial() => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/blob/master/documentation/PortForwarding.md")); \ No newline at end of file diff --git a/gui/lib/src/util/watch.dart b/gui/lib/src/util/watch.dart index dbf5e67..8b69854 100644 --- a/gui/lib/src/util/watch.dart +++ b/gui/lib/src/util/watch.dart @@ -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}); } } \ No newline at end of file diff --git a/gui/lib/src/widget/common/file_selector.dart b/gui/lib/src/widget/common/file_selector.dart index 972401a..1b22191 100644 --- a/gui/lib/src/widget/common/file_selector.dart +++ b/gui/lib/src/widget/common/file_selector.dart @@ -63,13 +63,19 @@ class _FileSelectorState extends State { _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); + } } \ No newline at end of file diff --git a/gui/lib/src/widget/common/setting_tile.dart b/gui/lib/src/widget/common/setting_tile.dart index c4f5935..683eb5d 100644 --- a/gui/lib/src/widget/common/setting_tile.dart +++ b/gui/lib/src/widget/common/setting_tile.dart @@ -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 { ), ); - Widget get _subtitle => Text( + Widget get _subtitle => widget.subtitle is Widget ? widget.subtitle : Text( widget.subtitle!, style: widget.subtitleStyle ?? FluentTheme.of(context).typography.body ); diff --git a/gui/lib/src/widget/game/start_button.dart b/gui/lib/src/widget/game/start_button.dart index 0ae8686..4a11756 100644 --- a/gui/lib/src/widget/game/start_button.dart +++ b/gui/lib/src/widget/game/start_button.dart @@ -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 { 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 { 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 { 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 { ); 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; } - 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) { + _matchmakerController.joinLocalHost(); + var accessible = await _checkAccessible(theme, gameServerPort); + if(!accessible) { 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 _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 { Future _removeHostEntry() async { await _supabase.from('hosts') .delete() - .match({'id': _gameController.uuid}); + .match({'id': _hostingController.uuid}); } Future _injectOrShowError(Injectable injectable, bool hosting) async { diff --git a/gui/lib/src/widget/home/profile.dart b/gui/lib/src/widget/home/profile.dart index 2ec4882..1645fdd 100644 --- a/gui/lib/src/widget/home/profile.dart +++ b/gui/lib/src/widget/home/profile.dart @@ -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'; diff --git a/gui/lib/src/widget/version/add_local_version.dart b/gui/lib/src/widget/version/add_local_version.dart index 15a4c51..6a876f5 100644 --- a/gui/lib/src/widget/version/add_local_version.dart +++ b/gui/lib/src/widget/version/add_local_version.dart @@ -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 { 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); } } }); diff --git a/gui/lib/src/widget/version/add_server_version.dart b/gui/lib/src/widget/version/add_server_version.dart index 3f58bb9..ac0b1b6 100644 --- a/gui/lib/src/widget/version/add_server_version.dart +++ b/gui/lib/src/widget/version/add_server_version.dart @@ -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 { 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(); } } diff --git a/gui/lib/src/widget/version/version_selector.dart b/gui/lib/src/widget/version/version_selector.dart index 06361aa..5821af7 100644 --- a/gui/lib/src/widget/version/version_selector.dart +++ b/gui/lib/src/widget/version/version_selector.dart @@ -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);