diff --git a/assets/images/auties.png b/assets/images/auties.png deleted file mode 100644 index b5fbf15..0000000 Binary files a/assets/images/auties.png and /dev/null differ diff --git a/assets/images/tutorial_else_1.png b/assets/images/tutorial_else_1.png new file mode 100644 index 0000000..b12929c Binary files /dev/null and b/assets/images/tutorial_else_1.png differ diff --git a/assets/images/tutorial_else_2.png b/assets/images/tutorial_else_2.png new file mode 100644 index 0000000..13813ae Binary files /dev/null and b/assets/images/tutorial_else_2.png differ diff --git a/assets/images/tutorial_else_3.png b/assets/images/tutorial_else_3.png new file mode 100644 index 0000000..7b8d8e8 Binary files /dev/null and b/assets/images/tutorial_else_3.png differ diff --git a/assets/images/tutorial_else_4.png b/assets/images/tutorial_else_4.png new file mode 100644 index 0000000..b025c32 Binary files /dev/null and b/assets/images/tutorial_else_4.png differ diff --git a/assets/images/tutorial_else_5.png b/assets/images/tutorial_else_5.png new file mode 100644 index 0000000..3162ea3 Binary files /dev/null and b/assets/images/tutorial_else_5.png differ diff --git a/assets/images/tutorial_else_6.png b/assets/images/tutorial_else_6.png new file mode 100644 index 0000000..cc31569 Binary files /dev/null and b/assets/images/tutorial_else_6.png differ diff --git a/assets/images/tutorial_else_7.png b/assets/images/tutorial_else_7.png new file mode 100644 index 0000000..1ff494f Binary files /dev/null and b/assets/images/tutorial_else_7.png differ diff --git a/assets/images/tutorial_else_8.png b/assets/images/tutorial_else_8.png new file mode 100644 index 0000000..0e96942 Binary files /dev/null and b/assets/images/tutorial_else_8.png differ diff --git a/assets/images/tutorial_own_1.png b/assets/images/tutorial_own_1.png new file mode 100644 index 0000000..b12929c Binary files /dev/null and b/assets/images/tutorial_own_1.png differ diff --git a/assets/images/tutorial_own_10.png b/assets/images/tutorial_own_10.png new file mode 100644 index 0000000..cc31569 Binary files /dev/null and b/assets/images/tutorial_own_10.png differ diff --git a/assets/images/tutorial_own_11.png b/assets/images/tutorial_own_11.png new file mode 100644 index 0000000..1ff494f Binary files /dev/null and b/assets/images/tutorial_own_11.png differ diff --git a/assets/images/tutorial_own_12.png b/assets/images/tutorial_own_12.png new file mode 100644 index 0000000..0e96942 Binary files /dev/null and b/assets/images/tutorial_own_12.png differ diff --git a/assets/images/tutorial_own_2.png b/assets/images/tutorial_own_2.png new file mode 100644 index 0000000..13813ae Binary files /dev/null and b/assets/images/tutorial_own_2.png differ diff --git a/assets/images/tutorial_own_3.png b/assets/images/tutorial_own_3.png new file mode 100644 index 0000000..7b8d8e8 Binary files /dev/null and b/assets/images/tutorial_own_3.png differ diff --git a/assets/images/tutorial_own_4.png b/assets/images/tutorial_own_4.png new file mode 100644 index 0000000..b025c32 Binary files /dev/null and b/assets/images/tutorial_own_4.png differ diff --git a/assets/images/tutorial_own_5.png b/assets/images/tutorial_own_5.png new file mode 100644 index 0000000..3162ea3 Binary files /dev/null and b/assets/images/tutorial_own_5.png differ diff --git a/assets/images/tutorial_own_6.png b/assets/images/tutorial_own_6.png new file mode 100644 index 0000000..79461fe Binary files /dev/null and b/assets/images/tutorial_own_6.png differ diff --git a/assets/images/tutorial_own_7.png b/assets/images/tutorial_own_7.png new file mode 100644 index 0000000..1ff494f Binary files /dev/null and b/assets/images/tutorial_own_7.png differ diff --git a/assets/images/tutorial_own_8.png b/assets/images/tutorial_own_8.png new file mode 100644 index 0000000..e4dbf8a Binary files /dev/null and b/assets/images/tutorial_own_8.png differ diff --git a/assets/images/tutorial_own_9.png b/assets/images/tutorial_own_9.png new file mode 100644 index 0000000..11f8b41 Binary files /dev/null and b/assets/images/tutorial_own_9.png differ diff --git a/lib/cli.dart b/lib/cli.dart index 47554a2..cabfa19 100644 --- a/lib/cli.dart +++ b/lib/cli.dart @@ -19,11 +19,7 @@ late String dll; late FortniteVersion version; late bool autoRestart; -void main(List args){ - handleCLI(args); -} - -Future handleCLI(List args) async { +void main(List args) async { stdout.writeln("Reboot Launcher"); stdout.writeln("Wrote by Auties00"); stdout.writeln("Version 5.3"); diff --git a/lib/main.dart b/lib/main.dart index d620f64..9704854 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart' show WinDesktopWindow; -import 'package:dart_vlc/dart_vlc.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -21,17 +20,10 @@ import 'package:window_manager/window_manager.dart'; final GlobalKey appKey = GlobalKey(); -void main(List args) async { +void main() async { await Directory(safeBinariesDirectory) .create(recursive: true); - if(args.isNotEmpty){ - handleCLI(args); - return; - } - WidgetsFlutterBinding.ensureInitialized(); - DartVLC.initialize(); - await SystemTheme.accentColor.load(); await GetStorage.init("game"); await GetStorage.init("server"); diff --git a/lib/src/controller/game_controller.dart b/lib/src/controller/game_controller.dart index 1eefe68..b036159 100644 --- a/lib/src/controller/game_controller.dart +++ b/lib/src/controller/game_controller.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:convert'; import 'dart:io'; @@ -6,6 +7,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart'; +import 'package:reboot_launcher/src/model/game_instance.dart'; import 'package:reboot_launcher/src/model/game_type.dart'; class GameController extends GetxController { @@ -15,11 +17,10 @@ class GameController extends GetxController { late final Rx> versions; late final Rxn _selectedVersion; late final Rx type; + late final HashMap gameInstancesMap; late final RxBool started; + late bool updated; Future? updater; - Process? gameProcess; - Process? launcherProcess; - Process? eacProcess; GameController() { _storage = GetStorage("game"); @@ -40,19 +41,22 @@ class GameController extends GetxController { type = Rx(GameType.values.elementAt(_storage.read("type") ?? 0)); type.listen((value) { _storage.write("type", value.index); - username.text = _storage.read("${type.value == GameType.client ? 'game' : 'host'}_username") ?? ""; + username.text = _readUsername(); }); - username = TextEditingController(text: _storage.read("${type.value == GameType.client ? 'game' : 'host'}_username") ?? ""); + username = TextEditingController(text: _readUsername()); username.addListener(() => _storage.write("${type.value == GameType.client ? 'game' : 'host'}_username", username.text)); + gameInstancesMap= HashMap(); + started = RxBool(false); + + updated = false; } - void kill() { - gameProcess?.kill(ProcessSignal.sigabrt); - launcherProcess?.kill(ProcessSignal.sigabrt); - eacProcess?.kill(ProcessSignal.sigabrt); + String _readUsername() { + var client = type.value == GameType.client; + return _storage.read("${client ? 'game' : 'host'}_username") ?? (client ? "" : "HostingServer"); } FortniteVersion? getVersionByName(String name) { @@ -86,6 +90,8 @@ class GameController extends GetxController { Rxn get selectedVersionObs => _selectedVersion; + GameInstance? get currentGameInstance => gameInstancesMap[type()]; + set selectedVersion(FortniteVersion? version) { _selectedVersion(version); _storage.write("version", version?.name); diff --git a/lib/src/controller/settings_controller.dart b/lib/src/controller/settings_controller.dart index 7b7bffc..87d5284 100644 --- a/lib/src/controller/settings_controller.dart +++ b/lib/src/controller/settings_controller.dart @@ -1,7 +1,7 @@ -import 'package:dart_vlc/dart_vlc.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:reboot_launcher/src/model/tutorial_page.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'dart:ui'; @@ -13,11 +13,13 @@ class SettingsController extends GetxController { late final TextEditingController authDll; late final TextEditingController matchmakingIp; late final Rx displayType; + late final RxBool doNotAskAgain; + late Rx tutorialPage; late double width; late double height; late double? offsetX; late double? offsetY; - Player? player; + late double scrollingDistance; SettingsController() { _storage = GetStorage("settings"); @@ -31,11 +33,18 @@ class SettingsController extends GetxController { _storage.write("ip", text); }); + doNotAskAgain = RxBool(_storage.read("do_not_ask_again") ?? false); + doNotAskAgain.listen((value) => _storage.write("do_not_ask_again", value)); + width = _storage.read("width") ?? window.physicalSize.width; height = _storage.read("height") ?? window.physicalSize.height; offsetX = _storage.read("offset_x"); offsetY = _storage.read("offset_y"); displayType = Rx(PaneDisplayMode.top); + + scrollingDistance = 0.0; + + tutorialPage = Rx(TutorialPage.start); } TextEditingController _createController(String key, String name) { diff --git a/lib/src/dialog/dialog.dart b/lib/src/dialog/dialog.dart index 2786522..09bb183 100644 --- a/lib/src/dialog/dialog.dart +++ b/lib/src/dialog/dialog.dart @@ -29,7 +29,7 @@ class GenericDialog extends AbstractDialog { ), ContentDialog( - style: ContentDialogThemeData( + style: ContentDialogThemeData( padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0) ), content: header, diff --git a/lib/src/dialog/server_dialogs.dart b/lib/src/dialog/server_dialogs.dart index 9b17554..38f18ef 100644 --- a/lib/src/dialog/server_dialogs.dart +++ b/lib/src/dialog/server_dialogs.dart @@ -1,6 +1,6 @@ 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_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/dialog/dialog.dart'; @@ -159,7 +159,7 @@ extension ServerControllerDialog on ServerController { builder: (context) => FutureBuilderDialog( future: Future.wait([ - pingSelf(port.text), + compute(pingSelf, port.text), Future.delayed(const Duration(seconds: 1)) ]), loadingMessage: "Pinging ${type().id} server...", diff --git a/lib/src/embedded/server.dart b/lib/src/embedded/server.dart index 2b8801c..131f3e5 100644 --- a/lib/src/embedded/server.dart +++ b/lib/src/embedded/server.dart @@ -105,7 +105,7 @@ Jaguar _createServer(String Function() ipQuery) { server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy); server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy); - return _addLoggingCapabilities(server); + return server; } Jaguar _createMatchmaker(){ var server = Jaguar(address: "127.0.0.1", port: 8080); diff --git a/lib/src/model/game_instance.dart b/lib/src/model/game_instance.dart new file mode 100644 index 0000000..521324c --- /dev/null +++ b/lib/src/model/game_instance.dart @@ -0,0 +1,15 @@ +import 'dart:io'; + +class GameInstance { + final Process gameProcess; + final Process? launcherProcess; + final Process? eacProcess; + + GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess); + + void kill() { + gameProcess.kill(ProcessSignal.sigabrt); + launcherProcess?.kill(ProcessSignal.sigabrt); + eacProcess?.kill(ProcessSignal.sigabrt); + } +} diff --git a/lib/src/model/tutorial_page.dart b/lib/src/model/tutorial_page.dart new file mode 100644 index 0000000..f8ffbd9 --- /dev/null +++ b/lib/src/model/tutorial_page.dart @@ -0,0 +1,5 @@ +enum TutorialPage { + start, + someoneElse, + yourOwn +} \ No newline at end of file diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart index 696f8e6..f213085 100644 --- a/lib/src/page/home_page.dart +++ b/lib/src/page/home_page.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:ui'; import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder; @@ -18,6 +17,7 @@ import 'package:reboot_launcher/src/widget/os/window_buttons.dart'; import 'package:window_manager/window_manager.dart'; import '../controller/settings_controller.dart'; +import '../model/tutorial_page.dart'; import 'info_page.dart'; class HomePage extends StatefulWidget { @@ -31,7 +31,6 @@ class _HomePageState extends State with WindowListener { static const double _headerSize = 48.0; static const double _sectionSize = 100.0; static const double _defaultPadding = 12.0; - static const double _openMenuSize = 320.0; static const int _headerButtonCount = 3; static const int _sectionButtonCount = 4; @@ -45,6 +44,7 @@ class _HomePageState extends State with WindowListener { final Rxn> _searchItems = Rxn(); final RxBool _focused = RxBool(true); final RxInt _index = RxInt(0); + bool _navigated = false; bool _shouldMaximize = false; @@ -125,7 +125,11 @@ class _HomePageState extends State with WindowListener { child: Obx(() => Stack( children: [ _createNavigationView(), - _createTitleBar(), + if(_settingsController.displayType() == PaneDisplayMode.top) + Align( + alignment: Alignment.topRight, + child: WindowTitleBar(focused: _focused()) + ), if(_settingsController.displayType() == PaneDisplayMode.top) _createTopDisplayGestures(), if(_focused() && isWin11) @@ -161,49 +165,82 @@ class _HomePageState extends State with WindowListener { child: child ); - NavigationView _createNavigationView() => NavigationView( - paneBodyBuilder: (body) => _createPage(body), - pane: NavigationPane( - size: const NavigationPaneSize( - topHeight: _headerSize + NavigationView _createNavigationView() { + return NavigationView( + paneBodyBuilder: (body) => _createPage(body), + pane: NavigationPane( + size: const NavigationPaneSize( + topHeight: _headerSize + ), + selected: _selectedIndex, + onChanged: _onIndexChanged, + displayMode: _settingsController.displayType(), + items: _createItems(), + indicator: const EndNavigationIndicator(), + footerItems: _createFooterItems(), + header: _settingsController.displayType() != PaneDisplayMode.open ? null : const SizedBox(height: _defaultPadding), + autoSuggestBox: _createAutoSuggestBox(), + autoSuggestBoxReplacement: _settingsController.displayType() == PaneDisplayMode.top ? null : const Icon(FluentIcons.search), ), - selected: _selectedIndex, - onChanged: (index) { - _settingsController.player?.pause(); - _index.value = index; - }, - displayMode: _settingsController.displayType(), - indicator: const EndNavigationIndicator(), - items: _createItems(), - footerItems: _createFooterItems(), - header: _settingsController.displayType() != PaneDisplayMode.open ? null : const SizedBox(height: _defaultPadding), - autoSuggestBox: _settingsController.displayType() == PaneDisplayMode.top ? null : TextBox( - key: _searchKey, - controller: _searchController, - placeholder: 'Search', - focusNode: _searchFocusNode - ), - autoSuggestBoxReplacement: _settingsController.displayType() == PaneDisplayMode.top ? null : const Icon(FluentIcons.search), - ), - onOpenSearch: () => _searchFocusNode.requestFocus(), - transitionBuilder: _settingsController.displayType() == PaneDisplayMode.top ? null : (child, animation) => child - ); + onOpenSearch: () => _searchFocusNode.requestFocus(), + transitionBuilder: _settingsController.displayType() == PaneDisplayMode.top ? null : (child, animation) => child + ); + } - RenderObjectWidget _createPage(Widget? body) => Padding( - padding: _createPagePadding(), - child: body - ); + void _onIndexChanged(int index) { + _index.value = index; + _navigated = true; + } - EdgeInsets _createPagePadding() { + TextBox? _createAutoSuggestBox() { if (_settingsController.displayType() == PaneDisplayMode.top) { - return const EdgeInsets.all(_defaultPadding); + return null; } - return const EdgeInsets.only( - top: 32, - left: _defaultPadding, - right: _defaultPadding, - bottom: _defaultPadding + return TextBox( + key: _searchKey, + controller: _searchController, + placeholder: 'Search', + focusNode: _searchFocusNode + ); + } + + RenderObjectWidget _createPage(Widget? body) { + if(_settingsController.displayType() == PaneDisplayMode.top){ + return Padding( + padding: const EdgeInsets.all(_defaultPadding), + child: body + ); + } + + return Column( + children: [ + Row( + children: [ + Expanded( + child: _createWindowGestures( + child: Container( + height: _headerSize, + color: Colors.transparent + ) + ) + ), + + WindowTitleBar(focused: _focused()) + ], + ), + + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: _defaultPadding, + right: _defaultPadding, + bottom: _defaultPadding + ), + child: body + ) + ) + ], ); } @@ -232,7 +269,8 @@ class _HomePageState extends State with WindowListener { PaneItem( title: const Text("Tutorial"), icon: const Icon(FluentIcons.info), - body: const InfoPage() + body: const InfoPage(), + onTap: _onTutorial ) ]; @@ -259,10 +297,22 @@ class _HomePageState extends State with WindowListener { PaneItem( title: const Text("Tutorial"), icon: const Icon(FluentIcons.info), - body: const InfoPage() + body: const InfoPage(), + onTap: _onTutorial ) ]; + void _onTutorial() { + if(!_navigated){ + setState(() { + _settingsController.tutorialPage.value = TutorialPage.start; + _settingsController.scrollingDistance = 0; + }); + } + + _navigated = false; + } + bool _calculateSize() { WidgetsBinding.instance.addPostFrameCallback((_) { _settingsController.saveWindowSize(); @@ -288,35 +338,5 @@ class _HomePageState extends State with WindowListener { return true; } - Widget _createTitleBar() => Align( - alignment: Alignment.topRight, - child: _createTitleBarContent(), - ); - - Widget _createTitleBarContent() { - if(_settingsController.displayType() == PaneDisplayMode.top) { - return WindowTitleBar(focused: _focused()); - } - - return Row( - children: [ - SizedBox( - width: _settingsController.displayType() == PaneDisplayMode.open ? _openMenuSize : _headerSize, - height: _headerSize - ), - - Expanded( - child: _createWindowGestures( - child: Container( - height: _headerSize, - color: Colors.transparent - ) - ) - ), - WindowTitleBar(focused: _focused()) - ], - ); - } - String get searchValue => _searchController.text; } diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart index 1222ddd..b98b0f5 100644 --- a/lib/src/page/info_page.dart +++ b/lib/src/page/info_page.dart @@ -1,11 +1,9 @@ - -import 'package:dart_vlc/dart_vlc.dart'; -import 'package:fluent_ui/fluent_ui.dart' hide Card; +import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controller/settings_controller.dart'; +import '../model/tutorial_page.dart'; class InfoPage extends StatefulWidget { const InfoPage({Key? key}) : super(key: key); @@ -15,36 +13,149 @@ class InfoPage extends StatefulWidget { } class _InfoPageState extends State { + final List _elseTitles = [ + "Open the settings tab", + "Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port", + "Open the home page", + "Type your username if you haven't already", + "Select the exact version that the host is using from the dropdown menu\n If necessary, install it using the download button", + "As you want to play, select client from the dropdown menu", + "Click launch to open the game", + "Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1" + ]; + final List _ownTitles = [ + "Open the settings tab", + "Type 127.0.0.1 as the matchmaking host", + "Open the home page", + "Type your username if you haven't already", + "Select the version you want to host\n If necessary, install it using the download button", + "As you want to host, select Headless Server from the dropdown menu\n If the headless server doesn't work for your version, use the normal server instead", + "Click launch to start the server and wait until the Reboot GUI shows up", + "To allow your friends to join your server, follow the instructions on playit.gg\n If you are an advanced user, open port 7777 on your router\n Finally, share your playit ip or public IPv4 address with your friends\n If you just want to play by yourself, skip this step", + "When you want to start the game, click on the 'Start Bus Countdown' button", + "If you also want to play, start a client by selecting Client from the dropdown menu\n Don't close or open again the launcher, use the same window", + "Click launch to open the game", + "Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1" + ]; + final SettingsController _settingsController = Get.find(); + late final ScrollController _controller; @override void initState() { - if(_settingsController.player == null){ - var player = Player(id: 1); - player.open( - Media.network("https://cdn.discordapp.com/attachments/1006260074416701450/1038844107986055190/tutorial.mp4") - ); - _settingsController.player = player; - } - - _settingsController.player?.play(); + _controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance); + _controller.addListener(() { + _settingsController.scrollingDistance = _controller.offset; + }); super.initState(); } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: double.infinity, - child: Card( - child: Video( - player: _settingsController.player, - height: MediaQuery.of(context).size.height * 0.85, - width: MediaQuery.of(context).size.width * 0.90, - scale: 1.0, - showControls: true, + switch(_settingsController.tutorialPage()) { + case TutorialPage.start: + return _createHomeScreen(); + case TutorialPage.someoneElse: + return _createInstructions(false); + case TutorialPage.yourOwn: + return _createInstructions(true); + } + } + + SizedBox _createInstructions(bool own) { + var titles = own ? _ownTitles : _elseTitles; + var codeName = own ? "own" : "else"; + return SizedBox.expand( + child: ListView.separated( + controller: _controller, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only( + right: 20.0 + ), + child: Card( + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + child: ListTile( + title: SelectableText("${index + 1}. ${titles[index]}"), + subtitle: Padding( + padding: const EdgeInsets.only(top: 12.0), + child: Image.asset("assets/images/tutorial_${codeName}_${index + 1}.png"), + ) + ) + ), + ), + separatorBuilder: (context, index) => const SizedBox(height: 8.0), + itemCount: titles.length, ) - ), + ); + } + + Widget _createHomeScreen() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _createCardWidget( + text: "Play on someone else's server", + description: "If one of your friends is hosting a game server, click here", + onClick: () => setState(() => _settingsController.tutorialPage.value = TutorialPage.someoneElse) + ), + + const SizedBox( + width: 8.0, + ), + + _createCardWidget( + text: "Host your own server", + description: "If you want to create your own server to invite your friends or to play around by yourself, click here", + onClick: () => setState(() => _settingsController.tutorialPage.value = TutorialPage.yourOwn) + ) + ] + ); + } + + Widget _createCardWidget({required String text, required String description, required Function() onClick}) { + return Expanded( + child: SizedBox( + height: double.infinity, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onClick, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + text, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold + ), + ), + + const SizedBox( + height: 8.0, + ), + + Text( + description, + textAlign: TextAlign.center + ), + ], + ) + ) + ) + ) + ) + ) ); } } \ No newline at end of file diff --git a/lib/src/page/launcher_page.dart b/lib/src/page/launcher_page.dart index 080ec43..a17cbe2 100644 --- a/lib/src/page/launcher_page.dart +++ b/lib/src/page/launcher_page.dart @@ -1,4 +1,6 @@ +import 'dart:async'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; @@ -30,7 +32,8 @@ class _LauncherPageState extends State { void initState() { if(_gameController.updater == null){ _gameController.updater = compute(downloadRebootDll, _updateTime) - ..then((value) => _updateTime = value); + ..then((value) => _updateTime = value) + ..then((value) => _gameController.updated = true); _buildController.cancelledDownload .listen((value) => value ? _onCancelWarning() : {}); } @@ -65,7 +68,7 @@ class _LauncherPageState extends State { return FutureBuilder( future: _gameController.updater ?? Future.value(true), builder: (context, snapshot) { - if (!snapshot.hasData && !snapshot.hasError) { + if (!_gameController.updated && !snapshot.hasData && !snapshot.hasError) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/src/page/settings_page.dart b/lib/src/page/settings_page.dart index 9c736c5..16661de 100644 --- a/lib/src/page/settings_page.dart +++ b/lib/src/page/settings_page.dart @@ -4,6 +4,7 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/dialog/snackbar.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/widget/shared/smart_switch.dart'; @@ -24,17 +25,16 @@ class SettingsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Tooltip( - message: "The hostname of the server that hosts the multiplayer matches", - child: Obx(() => SmartInput( - label: "Matchmaking Host", - placeholder: - "Type the hostname of the server that hosts the multiplayer matches", - controller: _settingsController.matchmakingIp, - validatorMode: AutovalidateMode.always, - validator: checkMatchmaking, - enabled: _serverController.type() == ServerType.embedded - )) - ), + message: + "The hostname of the server that hosts the multiplayer matches", + child: Obx(() => SmartInput( + label: "Matchmaking Host", + placeholder: + "Type the hostname of the server that hosts the multiplayer matches", + controller: _settingsController.matchmakingIp, + validatorMode: AutovalidateMode.always, + validator: checkMatchmaking, + enabled: _serverController.type() == ServerType.embedded))), Tooltip( message: "The dll that is injected when a server is launched", child: FileSelector( @@ -63,13 +63,25 @@ class SettingsPage extends StatelessWidget { message: "The dll that is injected to make the game work", child: FileSelector( label: "Cranium DLL", - placeholder: "Type the path to the dll used for authentication", + placeholder: + "Type the path to the dll used for authentication", controller: _settingsController.authDll, windowTitle: "Select a dll", folder: false, extension: "dll", validator: checkDll, - validatorMode: AutovalidateMode.always)) + validatorMode: AutovalidateMode.always)), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Version Status"), + const SizedBox(height: 6.0), + Button( + child: const Text("6.0${kDebugMode ? '-DEBUG' : '-RELEASE'}"), + onPressed: () => showMessage("What a nice launcher") + ) + ], + ) ]); } } diff --git a/lib/src/util/future.dart b/lib/src/util/future.dart deleted file mode 100644 index b7e96c8..0000000 --- a/lib/src/util/future.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:async'; - -extension FutureExtension on Future { - bool isCompleted() { - final completer = Completer(); - then(completer.complete).catchError(completer.completeError); - return completer.isCompleted; - } -} \ No newline at end of file diff --git a/lib/src/widget/home/game_type_selector.dart b/lib/src/widget/home/game_type_selector.dart index baabba7..df05b6f 100644 --- a/lib/src/widget/home/game_type_selector.dart +++ b/lib/src/widget/home/game_type_selector.dart @@ -35,7 +35,10 @@ class GameTypeSelector extends StatelessWidget { child: Text(type.name) ) ), - onPressed: () => _gameController.type(type) + onPressed: () { + _gameController.type(type); + _gameController.started.value = _gameController.currentGameInstance != null; + } ); } } diff --git a/lib/src/widget/home/launch_button.dart b/lib/src/widget/home/launch_button.dart index 9ea4ced..548489f 100644 --- a/lib/src/widget/home/launch_button.dart +++ b/lib/src/widget/home/launch_button.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:async/async.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; @@ -11,6 +10,7 @@ import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/dialog/dialog.dart'; import 'package:reboot_launcher/src/dialog/game_dialogs.dart'; import 'package:reboot_launcher/src/dialog/server_dialogs.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/model/game_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/util/os.dart'; @@ -21,9 +21,12 @@ import 'package:reboot_launcher/src/util/server.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; import 'package:path/path.dart' as path; -import '../../../main.dart'; -import '../../controller/settings_controller.dart'; -import '../../dialog/snackbar.dart'; +import 'package:reboot_launcher/src/../main.dart'; +import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/dialog/snackbar.dart'; +import 'package:reboot_launcher/src/model/game_instance.dart'; + +import '../shared/smart_check_box.dart'; class LaunchButton extends StatefulWidget { const LaunchButton( @@ -35,6 +38,7 @@ class LaunchButton extends StatefulWidget { } class _LaunchButtonState extends State { + final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()"; final List _errorStrings = [ "port 3551 failed: Connection refused", "Unable to login to Fortnite servers", @@ -43,7 +47,6 @@ class _LaunchButtonState extends State { "UOnlineAccountCommon::ForceLogout" ]; - final GameController _gameController = Get.find(); final ServerController _serverController = Get.find(); final SettingsController _settingsController = Get.find(); @@ -76,83 +79,180 @@ class _LaunchButtonState extends State { void _onPressed() async { if (_gameController.started()) { - _onStop(); - return; - } - - if (_gameController.username.text.isEmpty && _gameController.type() != GameType.client) { - showMessage("Missing username"); - _gameController.started.value = false; + _onStop(_gameController.type()); return; } _gameController.started.value = true; - if (_gameController.selectedVersionObs.value == null) { - showMessage("No version is selected"); - _gameController.started.value = false; - return; + if (_gameController.username.text.isEmpty) { + if(_serverController.type() != ServerType.local){ + showMessage("Missing username"); + _onStop(_gameController.type()); + return; + } + + showMessage("No username: expecting self sign in"); } + if (_gameController.selectedVersionObs.value == null) { + showMessage("No version is selected"); + _onStop(_gameController.type()); + return; + } + try { + await _resetLogFile(); + var version = _gameController.selectedVersionObs.value!; var gamePath = version.executable?.path; if(gamePath == null){ - _onError("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?", null); - _onStop(); + _onError("${version.location.path} no longer contains a Fortnite executable, did you delete it?", null); + _onStop(_gameController.type()); return; } - - if (version.launcher != null) { - _gameController.launcherProcess = await Process.start(version.launcher!.path, []); - Win32Process(_gameController.launcherProcess!.pid).suspend(); - } - - if (version.eacExecutable != null) { - _gameController.eacProcess = await Process.start(version.eacExecutable!.path, []); - Win32Process(_gameController.eacProcess!.pid).suspend(); - } - - var result = await _serverController.start( - required: true, - askPortKill: false, - ); + + var result = await _serverController.start(required: true, askPortKill: false); if(!result){ showMessage("Cannot launch the game as the backend didn't start up correctly"); - _onStop(); + _onStop(_gameController.type()); return; } - if(_logFile != null && await _logFile!.exists()){ - await _logFile!.delete(); - } - await compute(patchMatchmaking, version.executable!); await compute(patchHeadless, version.executable!); - var headlessHosting = _gameController.type() == GameType.headlessServer; - var arguments = createRebootArgs(_gameController.username.text, _gameController.type.value); - _gameController.gameProcess = await Process.start(gamePath, arguments) - ..exitCode.then((_) => _onEnd()) - ..outLines.forEach((line) => _onGameOutput(line)) - ..errLines.forEach((line) => _onGameOutput(line)); - _injectOrShowError(Injectable.cranium); - if(headlessHosting){ + await _startMatchMakingServer(); + await _startGameProcesses(version, _gameController.type()); + + if(_gameController.type() == GameType.headlessServer){ await _showServerLaunchingWarning(); } } catch (exception, stacktrace) { _closeDialogIfOpen(false); _onError(exception, stacktrace); - _onStop(); + _onStop(_gameController.type()); } } - void _onEnd() { + Future _startGameProcesses(FortniteVersion version, GameType type) async { + var launcherProcess = await _createLauncherProcess(version); + var eacProcess = await _createEacProcess(version); + var gameProcess = await _createGameProcess(version.executable!.path, type); + _gameController.gameInstancesMap[type] = GameInstance(gameProcess, launcherProcess, eacProcess); + _injectOrShowError(Injectable.cranium, type); + } + + Future _startMatchMakingServer() async { + if(_gameController.type() != GameType.client || _settingsController.doNotAskAgain()){ + return; + } + + var matchmakingIp = _settingsController.matchmakingIp.text; + if(!matchmakingIp.contains("127.0.0.1") && !matchmakingIp.contains("localhost")) { + return; + } + + var headlessServer = _gameController.gameInstancesMap[GameType.headlessServer] != null; + var server = _gameController.gameInstancesMap[GameType.server] != null; + if(headlessServer || server){ + return; + } + + var controller = CheckboxController(); + var result = await showDialog( + context: context, + builder: (context) => ContentDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + width: double.infinity, + child: Text( + "The matchmaking ip is set to the local machine, but no server is running. " + "If you want to start a match for your friends or just test out Reboot, you need to start a server, either now from this prompt or later manually.", + textAlign: TextAlign.start, + ) + ), + + const SizedBox(height: 12.0), + + SmartCheckBox( + controller: controller, + content: const Text("Don't ask again") + ) + ], + ), + actions: [ + Button( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Ignore'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Start a server'), + ) + ], + ) + ) ?? false; + _settingsController.doNotAskAgain.value = controller.value; + + if(!result){ + return; + } + + var version = _gameController.selectedVersionObs.value!; + _startGameProcesses( + version, + GameType.headlessServer + ); + } + + Future _createGameProcess(String gamePath, GameType type) async { + var gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, type)); + gameProcess + ..exitCode.then((_) => _onEnd(type)) + ..outLines.forEach((line) => _onGameOutput(line, type)) + ..errLines.forEach((line) => _onGameOutput(line, type)); + return gameProcess; + } + + Future _resetLogFile() async { + if(_logFile != null && await _logFile!.exists()){ + await _logFile!.delete(); + } + } + + Future _createLauncherProcess(FortniteVersion version) async { + var launcherFile = version.launcher; + if (launcherFile == null) { + return null; + } + + var launcherProcess = await Process.start(launcherFile.path, []); + Win32Process(launcherProcess.pid).suspend(); + return launcherProcess; + } + + Future _createEacProcess(FortniteVersion version) async { + var eacFile = version.eacExecutable; + if (eacFile == null) { + return null; + } + + var eacProcess = await Process.start(eacFile.path, []); + Win32Process(eacProcess.pid).suspend(); + return eacProcess; + } + + void _onEnd(GameType type) { if(_fail){ return; } _closeDialogIfOpen(false); - _onStop(); + _onStop(type); } void _closeDialogIfOpen(bool success) { @@ -169,27 +269,24 @@ class _LaunchButtonState extends State { context: appKey.currentContext!, builder: (context) => ProgressDialog( text: "Launching headless server...", - onStop: () { - Navigator.of(context).pop(false); - _onStop(); - } + onStop: () =>_onEnd(_gameController.type()) ) - ); + ) ?? false; - if(result != null && result){ + if(result){ return; } - _onStop(); + _onStop(_gameController.type()); } - void _onGameOutput(String line) { + void _onGameOutput(String line, GameType type) { if(_logFile != null){ _logFile!.writeAsString("$line\n", mode: FileMode.append); } - if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { - _onStop(); + if (line.contains(_shutdownLine)) { + _onStop(type); return; } @@ -205,14 +302,14 @@ class _LaunchButtonState extends State { } if(line.contains("Region ")){ - if(_gameController.type.value == GameType.client){ - _injectOrShowError(Injectable.console); + if(type == GameType.client){ + _injectOrShowError(Injectable.console, type); }else { - _injectOrShowError(Injectable.reboot) + _injectOrShowError(Injectable.reboot, type) .then((value) => _closeDialogIfOpen(true)); } - _injectOrShowError(Injectable.memoryFix); + _injectOrShowError(Injectable.memoryFix, type); } } @@ -239,13 +336,16 @@ class _LaunchButtonState extends State { ); } - void _onStop() { - _gameController.started.value = false; - _gameController.kill(); + void _onStop(GameType type) { + _gameController.gameInstancesMap[type]?.kill(); + _gameController.gameInstancesMap.remove(type); + if(type == _gameController.type()) { + _gameController.started.value = false; + } } - Future _injectOrShowError(Injectable injectable) async { - var gameProcess = _gameController.gameProcess; + Future _injectOrShowError(Injectable injectable, GameType type) async { + var gameProcess = _gameController.gameInstancesMap[type]?.gameProcess; if (gameProcess == null) { return; } @@ -255,25 +355,19 @@ class _LaunchButtonState extends State { if(!dllPath.existsSync()) { await _downloadMissingDll(injectable); if(!dllPath.existsSync()){ - _onDllFail(dllPath); + _onDllFail(dllPath, type); return; } } await injectDll(gameProcess.pid, dllPath.path); } catch (exception) { - showSnackbar( - appKey.currentContext!, - Snackbar( - content: Text("Cannot inject $injectable.dll: $exception", textAlign: TextAlign.center), - extended: true - ) - ); - _onStop(); + showMessage("Cannot inject $injectable.dll: $exception"); + _onStop(type); } } - void _onDllFail(File dllPath) { + void _onDllFail(File dllPath, GameType type) { WidgetsBinding.instance.addPostFrameCallback((_) { if(_fail){ return; @@ -282,7 +376,7 @@ class _LaunchButtonState extends State { _fail = true; _closeDialogIfOpen(false); showMissingDllError(path.basename(dllPath.path)); - _onStop(); + _onStop(type); }); } diff --git a/lib/src/widget/home/version_selector.dart b/lib/src/widget/home/version_selector.dart index 6bb58ec..9904d73 100644 --- a/lib/src/widget/home/version_selector.dart +++ b/lib/src/widget/home/version_selector.dart @@ -12,8 +12,8 @@ import 'package:reboot_launcher/src/dialog/add_local_version.dart'; import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../dialog/add_server_version.dart'; -import '../../util/checks.dart'; +import 'package:reboot_launcher/src/dialog/add_server_version.dart'; +import 'package:reboot_launcher/src/util/checks.dart'; import '../shared/file_selector.dart'; class VersionSelector extends StatefulWidget { diff --git a/lib/src/widget/shared/file_selector.dart b/lib/src/widget/shared/file_selector.dart index d6e65b7..eac22e9 100644 --- a/lib/src/widget/shared/file_selector.dart +++ b/lib/src/widget/shared/file_selector.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/dialog/snackbar.dart'; -import '../../util/selector.dart'; +import 'package:reboot_launcher/src/util/selector.dart'; class FileSelector extends StatefulWidget { final String label; diff --git a/pubspec.yaml b/pubspec.yaml index 7d10aea..8591219 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "5.4.0" +version: "6.0.0" publish_to: 'none' @@ -13,7 +13,7 @@ dependencies: bitsdojo_window: path: ./dependencies/bitsdojo_window-0.1.5 - fluent_ui: ^4.0.3+1 + fluent_ui: ^4.1.3 bitsdojo_window_windows: ^0.1.5 system_theme: ^2.0.0 http: ^0.13.5 @@ -41,7 +41,6 @@ dependencies: jaguar: ^3.1.3 hex: ^0.2.0 uuid: ^3.0.6 - dart_vlc: ^0.4.0 dependency_overrides: win32: ^3.0.0 @@ -67,7 +66,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 5.4.0.0 + msix_version: 6.0.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e8dc4a7..731d46f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -16,8 +15,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); - DartVlcPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DartVlcPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SystemThemePluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index bf449aa..7993f64 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_windows - dart_vlc screen_retriever system_theme url_launcher_windows diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 85c23db..c24283a 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -13,13 +13,37 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); #include #include +bool CheckOneInstance() +{ + HANDLE m_hStartEvent = CreateEventW( NULL, FALSE, FALSE, L"reboot_launcher"); + if(m_hStartEvent == NULL) + { + CloseHandle( m_hStartEvent ); + return false; + } + + if (GetLastError() == ERROR_ALREADY_EXISTS) + { + CloseHandle( m_hStartEvent ); + m_hStartEvent = NULL; + return false; + } + + return true; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { - std::vector command_line_arguments = GetCommandLineArguments(); - if (!command_line_arguments.empty() || (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())) { - CreateAndAttachConsole(); + if(!CheckOneInstance()){ + return false; } + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + std::vector command_line_arguments = GetCommandLineArguments(); + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } // Initialize COM, so that it is available for use in the library and/or // plugins. @@ -44,6 +68,5 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, } ::CoUninitialize(); - std::cout << "Done" << std::endl; return EXIT_SUCCESS; }