From e3b8d7d1828bea6c4cf8616056b8dbbc1afcc332 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Sat, 6 Jul 2024 18:43:52 +0200 Subject: [PATCH] Release 9.2.0 --- backend/index.js | 10 + backend/package-lock.json | 248 ++++++ backend/package.json | 3 +- backend/profiles/athena.json | 452 +++++----- backend/responses/friendslist.json | 7 + backend/responses/friendslist2.json | 9 + common/lib/common.dart | 4 +- common/lib/src/constant/backend.dart | 3 +- common/lib/src/extension/path.dart | 37 +- common/lib/src/model/fortnite_build.dart | 10 +- common/lib/src/model/fortnite_server.dart | 47 ++ common/lib/src/model/fortnite_version.dart | 13 +- common/lib/src/model/game_instance.dart | 26 +- common/lib/src/model/server_result.dart | 8 +- common/lib/src/util/backend.dart | 38 +- common/lib/src/util/build.dart | 37 +- {gui => common}/lib/src/util/log.dart | 6 +- common/lib/src/util/path.dart | 2 +- common/lib/src/util/process.dart | 4 +- common/pubspec.yaml | 1 + gui/assets/backend/lawinserver.exe | Bin 85528026 -> 86146729 bytes .../info/en/faq/1. What is Project Reboot | 3 - gui/assets/info/en/faq/10. Corrupted build | 1 - ...nds with email and password authentication | 3 - .../info/en/faq/12. Can I get skins in game | 2 - .../en/faq/2. What is a Fortnite game server | 7 - .../en/faq/3. Types of Fortnite game server | 4 - .../faq/4. How can others join my game server | 22 - gui/assets/info/en/faq/5. What is a backend | 6 - .../faq/6. What is the Unreal Engine console | 4 - ...ortnite because of an authentication error | 4 - ... see two Fortnite versions opened on my PC | 2 - ...nnot enter in a match when I'm in Fortnite | 3 - .../en/questions/1. What is Project Reboot | 3 - ...is a bug that you should report on Discord | 3 - ...an't enter in game from the Fortnite lobby | 3 - ...n authentication error, what should you do | 3 - .../en/questions/13. Can I have skins in-game | 3 - .../questions/2. Which seasons are supported | 3 - .../info/en/questions/3. What is 127.0.0.1 | 3 - .../4. What is a Fortnite game server | 3 - ... I play if I haven't started a game server | 3 - .../6. What is an headless game server | 3 - ...hy do I see two Fortnite games when I play | 3 - .../8. How can other players join my game | 3 - .../info/en/questions/9. What is a backend | 3 - gui/lib/l10n/reboot_en.arb | 94 ++- gui/lib/main.dart | 64 +- .../src/controller/backend_controller.dart | 20 +- gui/lib/src/controller/build_controller.dart | 22 - gui/lib/src/controller/game_controller.dart | 17 +- .../src/controller/hosting_controller.dart | 92 +- .../src/controller/settings_controller.dart | 324 ++++++- gui/lib/src/controller/update_controller.dart | 161 ---- .../src/dialog/abstract/dialog_button.dart | 56 -- .../abstract/dialog.dart | 85 +- .../abstract/info_bar.dart | 61 +- gui/lib/src/messenger/abstract/overlay.dart | 170 ++++ .../implementation/data.dart | 5 +- .../implementation/dll.dart | 5 +- .../implementation/error.dart | 11 +- .../src/messenger/implementation/onboard.dart | 346 ++++++++ .../implementation/profile.dart | 13 +- .../implementation/server.dart | 165 ++-- .../src/messenger/implementation/version.dart | 463 ++++++++++ .../src/page/implementation/backend_page.dart | 61 +- .../src/page/implementation/browser_page.dart | 359 ++++++++ .../src/page/implementation/home_page.dart | 791 +++++++++--------- .../{server_host_page.dart => host_page.dart} | 345 ++++---- .../src/page/implementation/info_page.dart | 318 ++----- .../src/page/implementation/play_page.dart | 102 +-- .../implementation/server_browser_page.dart | 237 ------ .../page/implementation/settings_page.dart | 114 +-- gui/lib/src/page/pages.dart | 24 +- gui/lib/src/util/checks.dart | 86 -- gui/lib/src/util/dll.dart | 107 --- gui/lib/src/util/matchmaker.dart | 79 +- gui/lib/src/util/os.dart | 2 +- gui/lib/src/widget/add_local_version.dart | 103 --- gui/lib/src/widget/add_server_version.dart | 347 -------- gui/lib/src/widget/file_setting_tile.dart | 27 +- gui/lib/src/widget/game_start_button.dart | 185 ++-- gui/lib/src/widget/profile_tile.dart | 20 +- gui/lib/src/widget/server_start_button.dart | 2 +- gui/lib/src/widget/server_type_selector.dart | 23 +- gui/lib/src/widget/setting_tile.dart | 157 ++-- gui/lib/src/widget/version_name_input.dart | 24 - gui/lib/src/widget/version_selector.dart | 225 ++--- gui/lib/src/widget/version_selector_tile.dart | 10 +- gui/pubspec.yaml | 8 +- .../exe/custom-inno-setup-script.iss | 8 +- 91 files changed, 3871 insertions(+), 3132 deletions(-) create mode 100644 common/lib/src/model/fortnite_server.dart rename {gui => common}/lib/src/util/log.dart (71%) delete mode 100644 gui/assets/info/en/faq/1. What is Project Reboot delete mode 100644 gui/assets/info/en/faq/10. Corrupted build delete mode 100644 gui/assets/info/en/faq/11. LawinV2 and backends with email and password authentication delete mode 100644 gui/assets/info/en/faq/12. Can I get skins in game delete mode 100644 gui/assets/info/en/faq/2. What is a Fortnite game server delete mode 100644 gui/assets/info/en/faq/3. Types of Fortnite game server delete mode 100644 gui/assets/info/en/faq/4. How can others join my game server delete mode 100644 gui/assets/info/en/faq/5. What is a backend delete mode 100644 gui/assets/info/en/faq/6. What is the Unreal Engine console delete mode 100644 gui/assets/info/en/faq/7. I cannot open Fortnite because of an authentication error delete mode 100644 gui/assets/info/en/faq/8. Why do I see two Fortnite versions opened on my PC delete mode 100644 gui/assets/info/en/faq/9. I cannot enter in a match when I'm in Fortnite delete mode 100644 gui/assets/info/en/questions/1. What is Project Reboot delete mode 100644 gui/assets/info/en/questions/10. Which of these is a bug that you should report on Discord delete mode 100644 gui/assets/info/en/questions/11. I can't enter in game from the Fortnite lobby delete mode 100644 gui/assets/info/en/questions/12. If you get an authentication error, what should you do delete mode 100644 gui/assets/info/en/questions/13. Can I have skins in-game delete mode 100644 gui/assets/info/en/questions/2. Which seasons are supported delete mode 100644 gui/assets/info/en/questions/3. What is 127.0.0.1 delete mode 100644 gui/assets/info/en/questions/4. What is a Fortnite game server delete mode 100644 gui/assets/info/en/questions/5. Can I play if I haven't started a game server delete mode 100644 gui/assets/info/en/questions/6. What is an headless game server delete mode 100644 gui/assets/info/en/questions/7. Why do I see two Fortnite games when I play delete mode 100644 gui/assets/info/en/questions/8. How can other players join my game delete mode 100644 gui/assets/info/en/questions/9. What is a backend delete mode 100644 gui/lib/src/controller/build_controller.dart delete mode 100644 gui/lib/src/controller/update_controller.dart delete mode 100644 gui/lib/src/dialog/abstract/dialog_button.dart rename gui/lib/src/{dialog => messenger}/abstract/dialog.dart (74%) rename gui/lib/src/{dialog => messenger}/abstract/info_bar.dart (56%) create mode 100644 gui/lib/src/messenger/abstract/overlay.dart rename gui/lib/src/{dialog => messenger}/implementation/data.dart (74%) rename gui/lib/src/{dialog => messenger}/implementation/dll.dart (72%) rename gui/lib/src/{dialog => messenger}/implementation/error.dart (81%) create mode 100644 gui/lib/src/messenger/implementation/onboard.dart rename gui/lib/src/{dialog => messenger}/implementation/profile.dart (88%) rename gui/lib/src/{dialog => messenger}/implementation/server.dart (69%) create mode 100644 gui/lib/src/messenger/implementation/version.dart create mode 100644 gui/lib/src/page/implementation/browser_page.dart rename gui/lib/src/page/implementation/{server_host_page.dart => host_page.dart} (71%) delete mode 100644 gui/lib/src/page/implementation/server_browser_page.dart delete mode 100644 gui/lib/src/util/checks.dart delete mode 100644 gui/lib/src/util/dll.dart delete mode 100644 gui/lib/src/widget/add_local_version.dart delete mode 100644 gui/lib/src/widget/add_server_version.dart delete mode 100644 gui/lib/src/widget/version_name_input.dart diff --git a/backend/index.js b/backend/index.js index fe3d4eb..6cdb596 100644 --- a/backend/index.js +++ b/backend/index.js @@ -4,6 +4,16 @@ const fs = require("fs"); const path = require("path"); const cookieParser = require("cookie-parser"); +const audit = require('express-requests-logger') +express.use(audit({ + request: { + maxBodyLength: 150 + }, + response: { + maxBodyLength: 150 + } +})); + express.use(Express.json()); express.use(Express.urlencoded({ extended: true })); express.use(Express.static('public')); diff --git a/backend/package-lock.json b/backend/package-lock.json index f4655aa..f25d734 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "cookie-parser": "^1.4.6", "express": "^4.18.2", + "express-requests-logger": "^4.0.0", "ini": "^2.0.0", "nexe": "^4.0.0-rc.6", "path": "^0.12.7", @@ -388,6 +389,23 @@ "node": ">v0.4.12" } }, + "node_modules/bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "engines": [ + "node >=0.10.0" + ], + "bin": { + "bunyan": "bin/bunyan" + }, + "optionalDependencies": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1063,6 +1081,19 @@ "node": ">=4" } }, + "node_modules/dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "^2.14.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -1168,6 +1199,16 @@ "node": ">= 0.10.0" } }, + "node_modules/express-requests-logger": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/express-requests-logger/-/express-requests-logger-4.0.0.tgz", + "integrity": "sha512-NHQptnDY0fceiTSWLnW0dbJSFlrvbFpCGHmY6LsTMmJLgkyO3x8qAJ+EsryQRMga20YH8Ynt/vnmg23QP07h1Q==", + "dependencies": { + "bunyan": "^1.8.14", + "flat": "^5.0.2", + "lodash": "^4.17.14" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -1292,6 +1333,14 @@ "node": ">= 0.8" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -1788,6 +1837,11 @@ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -1973,6 +2027,15 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/moo-server": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz", @@ -2009,6 +2072,77 @@ "readable-stream": "^3.6.0" } }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "optional": true, + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/mv/node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mv/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mv/node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "optional": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "optional": true, + "bin": { + "ncp": "bin/ncp" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2534,6 +2668,12 @@ } ] }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "node_modules/safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", @@ -3405,6 +3545,17 @@ "wrench": "1.3.x" } }, + "bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3941,6 +4092,15 @@ } } }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, "duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -4038,6 +4198,16 @@ } } }, + "express-requests-logger": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/express-requests-logger/-/express-requests-logger-4.0.0.tgz", + "integrity": "sha512-NHQptnDY0fceiTSWLnW0dbJSFlrvbFpCGHmY6LsTMmJLgkyO3x8qAJ+EsryQRMga20YH8Ynt/vnmg23QP07h1Q==", + "requires": { + "bunyan": "^1.8.14", + "flat": "^5.0.2", + "lodash": "^4.17.14" + } + }, "ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -4130,6 +4300,11 @@ "unpipe": "~1.0.0" } }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, "fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -4496,6 +4671,11 @@ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -4626,6 +4806,12 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "optional": true + }, "moo-server": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz", @@ -4645,6 +4831,62 @@ "readable-stream": "^3.6.0" } }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "optional": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, + "nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "optional": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "optional": true + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -5000,6 +5242,12 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "safe-stable-stringify": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", diff --git a/backend/package.json b/backend/package.json index be6e230..1d54fbc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,7 +12,8 @@ "uuid": "^8.3.2", "ws": "^8.5.0", "xml-parser": "^1.2.1", - "xmlbuilder": "^15.1.1" + "xmlbuilder": "^15.1.1", + "express-requests-logger": "^4.0.0" }, "scripts": { "start": "node index.js", diff --git a/backend/profiles/athena.json b/backend/profiles/athena.json index 82acebe..97424d8 100644 --- a/backend/profiles/athena.json +++ b/backend/profiles/athena.json @@ -2,7 +2,7 @@ "_id": "LawinServer", "created": "0001-01-01T00:00:00.000Z", "updated": "0001-01-01T00:00:00.000Z", - "rvn": 21, + "rvn": 23, "wipeNumber": 1, "accountId": "LawinServer", "profileId": "athena", @@ -131978,7 +131978,7 @@ "S4-ChallengeBundleSchedule:Season4_Challenge_Schedule": { "templateId": "ChallengeBundleSchedule:Season4_Challenge_Schedule", "attributes": { - "unlock_epoch": "2024-05-31T19:57:34.940Z", + "unlock_epoch": "2024-06-24T20:17:00.089Z", "max_level_bonus": 0, "level": 1, "item_seen": true, @@ -132004,7 +132004,7 @@ "S4-ChallengeBundleSchedule:Season4_ProgressiveB_Schedule": { "templateId": "ChallengeBundleSchedule:Season4_ProgressiveB_Schedule", "attributes": { - "unlock_epoch": "2024-05-31T19:57:34.940Z", + "unlock_epoch": "2024-06-24T20:17:00.089Z", "max_level_bonus": 0, "level": 1, "item_seen": true, @@ -132019,7 +132019,7 @@ "S4-ChallengeBundleSchedule:Season4_StarterChallenge_Schedule": { "templateId": "ChallengeBundleSchedule:Season4_StarterChallenge_Schedule", "attributes": { - "unlock_epoch": "2024-05-31T19:57:34.940Z", + "unlock_epoch": "2024-06-24T20:17:00.089Z", "max_level_bonus": 0, "level": 1, "item_seen": true, @@ -132404,7 +132404,7 @@ "S4-Quest:Quest_BR_S4_Cumulative_CollectTokens_01": { "templateId": "Quest:Quest_BR_S4_Cumulative_CollectTokens_01", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132415,7 +132415,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132428,7 +132428,7 @@ "S4-Quest:Quest_BR_S4_Cumulative_CollectTokens_02": { "templateId": "Quest:Quest_BR_S4_Cumulative_CollectTokens_02", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132439,7 +132439,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132452,7 +132452,7 @@ "S4-Quest:Quest_BR_S4_Cumulative_CollectTokens_03": { "templateId": "Quest:Quest_BR_S4_Cumulative_CollectTokens_03", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132463,7 +132463,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132476,7 +132476,7 @@ "S4-Quest:Quest_BR_S4_Cumulative_CollectTokens_04": { "templateId": "Quest:Quest_BR_S4_Cumulative_CollectTokens_04", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132487,7 +132487,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132500,7 +132500,7 @@ "S4-Quest:Quest_BR_S4_Cumulative_CollectTokens_05": { "templateId": "Quest:Quest_BR_S4_Cumulative_CollectTokens_05", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132511,7 +132511,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132524,7 +132524,7 @@ "S4-Quest:Quest_BR_S4_Cumulative_CollectTokens_06": { "templateId": "Quest:Quest_BR_S4_Cumulative_CollectTokens_06", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132535,7 +132535,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132548,7 +132548,7 @@ "S4-Quest:Quest_BR_S4_Cumulative_CollectTokens_07": { "templateId": "Quest:Quest_BR_S4_Cumulative_CollectTokens_07", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132559,7 +132559,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132572,7 +132572,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_01": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_01", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132583,7 +132583,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132596,7 +132596,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_02": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_02", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132607,7 +132607,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132620,7 +132620,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_03": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_03", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132631,7 +132631,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132644,7 +132644,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_04": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_04", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132655,7 +132655,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132668,7 +132668,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_05": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveA_05", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132679,7 +132679,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132692,7 +132692,7 @@ "S4-Quest:Quest_BR_Damage_SniperRifle": { "templateId": "Quest:Quest_BR_Damage_SniperRifle", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132703,7 +132703,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132716,7 +132716,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_FlushFactory": { "templateId": "Quest:Quest_BR_Eliminate_Location_FlushFactory", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132727,7 +132727,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132740,7 +132740,7 @@ "S4-Quest:Quest_BR_Eliminate_Weapon_Pistol": { "templateId": "Quest:Quest_BR_Eliminate_Weapon_Pistol", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132751,7 +132751,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132764,7 +132764,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_HauntedHills": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_HauntedHills", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132775,7 +132775,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132788,7 +132788,7 @@ "S4-Quest:Quest_BR_Interact_FORTNITE": { "templateId": "Quest:Quest_BR_Interact_FORTNITE", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132799,7 +132799,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132831,7 +132831,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_01": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_01", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132842,7 +132842,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132855,7 +132855,7 @@ "S4-Quest:Quest_BR_S4W1_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W1_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132866,7 +132866,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132885,7 +132885,7 @@ "S4-Quest:Quest_BR_Use_PortaFort": { "templateId": "Quest:Quest_BR_Use_PortaFort", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132896,7 +132896,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132909,7 +132909,7 @@ "S4-Quest:Quest_BR_Damage_SuppressedWeapon": { "templateId": "Quest:Quest_BR_Damage_SuppressedWeapon", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132920,7 +132920,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132933,7 +132933,7 @@ "S4-Quest:Quest_BR_Dance_ScavengerHunt_Cameras": { "templateId": "Quest:Quest_BR_Dance_ScavengerHunt_Cameras", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132944,7 +132944,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132967,7 +132967,7 @@ "S4-Quest:Quest_BR_Eliminate_ExplosiveWeapon": { "templateId": "Quest:Quest_BR_Eliminate_ExplosiveWeapon", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -132978,7 +132978,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -132991,7 +132991,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_TomatoTown": { "templateId": "Quest:Quest_BR_Eliminate_Location_TomatoTown", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133002,7 +133002,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133015,7 +133015,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_GreasyGrove": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_GreasyGrove", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133026,7 +133026,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133039,7 +133039,7 @@ "S4-Quest:Quest_BR_Interact_GravityStones": { "templateId": "Quest:Quest_BR_Interact_GravityStones", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133050,7 +133050,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133063,7 +133063,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_01": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_01", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133074,7 +133074,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133087,7 +133087,7 @@ "S4-Quest:Quest_BR_S4W2_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W2_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133098,7 +133098,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133117,7 +133117,7 @@ "S4-Quest:Quest_BR_Damage_Pistol": { "templateId": "Quest:Quest_BR_Damage_Pistol", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133128,7 +133128,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133141,7 +133141,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_TiltedTowers": { "templateId": "Quest:Quest_BR_Eliminate_Location_TiltedTowers", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133152,7 +133152,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133165,7 +133165,7 @@ "S4-Quest:Quest_BR_Eliminate_Weapon_SniperRifle": { "templateId": "Quest:Quest_BR_Eliminate_Weapon_SniperRifle", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133176,7 +133176,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133189,7 +133189,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_LonelyLodge": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_LonelyLodge", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133200,7 +133200,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133213,7 +133213,7 @@ "S4-Quest:Quest_BR_Interact_RubberDuckies": { "templateId": "Quest:Quest_BR_Interact_RubberDuckies", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133224,7 +133224,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133256,7 +133256,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_02": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_02", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133267,7 +133267,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133280,7 +133280,7 @@ "S4-Quest:Quest_BR_Revive": { "templateId": "Quest:Quest_BR_Revive", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133291,7 +133291,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133304,7 +133304,7 @@ "S4-Quest:Quest_BR_S4W3_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W3_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133315,7 +133315,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133334,7 +133334,7 @@ "S4-Quest:Quest_BR_Damage_AssaultRifle": { "templateId": "Quest:Quest_BR_Damage_AssaultRifle", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133345,7 +133345,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133358,7 +133358,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_SnobbyShores": { "templateId": "Quest:Quest_BR_Eliminate_Location_SnobbyShores", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133369,7 +133369,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.940Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133382,7 +133382,7 @@ "S4-Quest:Quest_BR_Eliminate_Trap": { "templateId": "Quest:Quest_BR_Eliminate_Trap", "attributes": { - "creation_time": "2024-05-31T19:57:34.940Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133393,7 +133393,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133406,7 +133406,7 @@ "S4-Quest:Quest_BR_Interact_AmmoBoxes_SingleMatch": { "templateId": "Quest:Quest_BR_Interact_AmmoBoxes_SingleMatch", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133417,7 +133417,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133430,7 +133430,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_WailingWoods": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_WailingWoods", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133441,7 +133441,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133454,7 +133454,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_02": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_02", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133465,7 +133465,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133478,7 +133478,7 @@ "S4-Quest:Quest_BR_S4W4_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W4_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133489,7 +133489,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133508,7 +133508,7 @@ "S4-Quest:Quest_BR_Visit_ScavengerHunt_StormCircles": { "templateId": "Quest:Quest_BR_Visit_ScavengerHunt_StormCircles", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133519,7 +133519,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133532,7 +133532,7 @@ "S4-Quest:Quest_BR_Damage_SMG": { "templateId": "Quest:Quest_BR_Damage_SMG", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133543,7 +133543,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133556,7 +133556,7 @@ "S4-Quest:Quest_BR_Dance_ScavengerHunt_Platforms": { "templateId": "Quest:Quest_BR_Dance_ScavengerHunt_Platforms", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133567,7 +133567,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133580,7 +133580,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_LuckyLanding": { "templateId": "Quest:Quest_BR_Eliminate_Location_LuckyLanding", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133591,7 +133591,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133604,7 +133604,7 @@ "S4-Quest:Quest_BR_Eliminate_Weapon_MinigunLMG": { "templateId": "Quest:Quest_BR_Eliminate_Weapon_MinigunLMG", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133615,7 +133615,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133628,7 +133628,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_DustyDivot": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_DustyDivot", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133639,7 +133639,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133652,7 +133652,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_03": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_03", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133663,7 +133663,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133676,7 +133676,7 @@ "S4-Quest:Quest_BR_S4W5_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W5_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133687,7 +133687,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133706,7 +133706,7 @@ "S4-Quest:Quest_BR_Use_VendingMachine": { "templateId": "Quest:Quest_BR_Use_VendingMachine", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133717,7 +133717,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133730,7 +133730,7 @@ "S4-Quest:Quest_BR_Damage_Shotgun": { "templateId": "Quest:Quest_BR_Damage_Shotgun", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133741,7 +133741,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133754,7 +133754,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_RetailRow": { "templateId": "Quest:Quest_BR_Eliminate_Location_RetailRow", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133765,7 +133765,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133778,7 +133778,7 @@ "S4-Quest:Quest_BR_Eliminate_Weapon_SMG": { "templateId": "Quest:Quest_BR_Eliminate_Weapon_SMG", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133789,7 +133789,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133802,7 +133802,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_LootLake": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_LootLake", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133813,7 +133813,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133826,7 +133826,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_03": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_03", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133837,7 +133837,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133850,7 +133850,7 @@ "S4-Quest:Quest_BR_Interact_SupplyDrops": { "templateId": "Quest:Quest_BR_Interact_SupplyDrops", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133861,7 +133861,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133874,7 +133874,7 @@ "S4-Quest:Quest_BR_S4W6_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W6_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133885,7 +133885,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133904,7 +133904,7 @@ "S4-Quest:Quest_BR_Spray_SpecificTargets": { "templateId": "Quest:Quest_BR_Spray_SpecificTargets", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133915,7 +133915,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133943,7 +133943,7 @@ "S4-Quest:Quest_BR_Damage_Pickaxe": { "templateId": "Quest:Quest_BR_Damage_Pickaxe", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133954,7 +133954,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133967,7 +133967,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_ShiftyShafts": { "templateId": "Quest:Quest_BR_Eliminate_Location_ShiftyShafts", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -133978,7 +133978,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -133991,7 +133991,7 @@ "S4-Quest:Quest_BR_Eliminate_Weapon_AssaultRifle": { "templateId": "Quest:Quest_BR_Eliminate_Weapon_AssaultRifle", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -134002,7 +134002,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134015,7 +134015,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_RiskyReels": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_RiskyReels", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -134026,7 +134026,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134039,7 +134039,7 @@ "S4-Quest:Quest_BR_Interact_ForagedItems": { "templateId": "Quest:Quest_BR_Interact_ForagedItems", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -134050,7 +134050,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134063,7 +134063,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_04": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_04", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -134074,7 +134074,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134087,7 +134087,7 @@ "S4-Quest:Quest_BR_S4W7_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W7_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.090Z", "level": -1, "item_seen": true, "playlists": [], @@ -134098,7 +134098,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.090Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134117,7 +134117,7 @@ "S4-Quest:Quest_BR_Score_Goals": { "templateId": "Quest:Quest_BR_Score_Goals", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134128,7 +134128,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134147,7 +134147,7 @@ "S4-Quest:Quest_BR_Damage_Headshot": { "templateId": "Quest:Quest_BR_Damage_Headshot", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134158,7 +134158,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134171,7 +134171,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_PleasantPark": { "templateId": "Quest:Quest_BR_Eliminate_Location_PleasantPark", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134182,7 +134182,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134195,7 +134195,7 @@ "S4-Quest:Quest_BR_Eliminate_SuppressedWeapon": { "templateId": "Quest:Quest_BR_Eliminate_SuppressedWeapon", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134206,7 +134206,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134219,7 +134219,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_SaltySprings": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_SaltySprings", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134230,7 +134230,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134243,7 +134243,7 @@ "S4-Quest:Quest_BR_Interact_Chests_SingleMatch": { "templateId": "Quest:Quest_BR_Interact_Chests_SingleMatch", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134254,7 +134254,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134267,7 +134267,7 @@ "S4-Quest:Quest_BR_Interact_GniceGnomes": { "templateId": "Quest:Quest_BR_Interact_GniceGnomes", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134278,7 +134278,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134304,7 +134304,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_04": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_04", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134315,7 +134315,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134328,7 +134328,7 @@ "S4-Quest:Quest_BR_S4W8_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W8_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134339,7 +134339,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134358,7 +134358,7 @@ "S4-Quest:Quest_BR_Damage_ExplosiveWeapon": { "templateId": "Quest:Quest_BR_Damage_ExplosiveWeapon", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134369,7 +134369,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134382,7 +134382,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_AnarchyAcres": { "templateId": "Quest:Quest_BR_Eliminate_Location_AnarchyAcres", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134393,7 +134393,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134406,7 +134406,7 @@ "S4-Quest:Quest_BR_Eliminate_Weapon_Shotgun": { "templateId": "Quest:Quest_BR_Eliminate_Weapon_Shotgun", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134417,7 +134417,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134430,7 +134430,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_MoistyMire": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_MoistyMire", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134441,7 +134441,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134454,7 +134454,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_05": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_TreasureMap_05", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134465,7 +134465,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134478,7 +134478,7 @@ "S4-Quest:Quest_BR_S4W9_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W9_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134489,7 +134489,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134508,7 +134508,7 @@ "S4-Quest:Quest_BR_Use_ShoppingCart": { "templateId": "Quest:Quest_BR_Use_ShoppingCart", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134519,7 +134519,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134532,7 +134532,7 @@ "S4-Quest:Quest_BR_Visit_NamedLocations_SingleMatch": { "templateId": "Quest:Quest_BR_Visit_NamedLocations_SingleMatch", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134543,7 +134543,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134575,7 +134575,7 @@ "S4-Quest:Quest_BR_Damage_Enemy_Buildings": { "templateId": "Quest:Quest_BR_Damage_Enemy_Buildings", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134586,7 +134586,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134599,7 +134599,7 @@ "S4-Quest:Quest_BR_Eliminate": { "templateId": "Quest:Quest_BR_Eliminate", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134610,7 +134610,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134623,7 +134623,7 @@ "S4-Quest:Quest_BR_Eliminate_Location_FatalFields": { "templateId": "Quest:Quest_BR_Eliminate_Location_FatalFields", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134634,7 +134634,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134647,7 +134647,7 @@ "S4-Quest:Quest_BR_Interact_ChestAmmoBoxSupplyDrop_SingleMatch": { "templateId": "Quest:Quest_BR_Interact_ChestAmmoBoxSupplyDrop_SingleMatch", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134658,7 +134658,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134673,7 +134673,7 @@ "S4-Quest:Quest_BR_Interact_Chests_Location_JunkJunction": { "templateId": "Quest:Quest_BR_Interact_Chests_Location_JunkJunction", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134684,7 +134684,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134697,7 +134697,7 @@ "S4-Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_05": { "templateId": "Quest:Quest_BR_Interact_ScavengerHunt_Triangulate_05", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134708,7 +134708,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134721,7 +134721,7 @@ "S4-Quest:Quest_BR_S4W10_Cumulative_CompleteAll": { "templateId": "Quest:Quest_BR_S4W10_Cumulative_CompleteAll", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134732,7 +134732,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134751,7 +134751,7 @@ "S4-Quest:Quest_BR_Skydive_Rings": { "templateId": "Quest:Quest_BR_Skydive_Rings", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134762,7 +134762,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134775,7 +134775,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_01": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_01", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134786,7 +134786,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134799,7 +134799,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_02": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_02", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134810,7 +134810,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134823,7 +134823,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_03": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_03", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134834,7 +134834,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134847,7 +134847,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_04": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_04", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134858,7 +134858,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134871,7 +134871,7 @@ "S4-Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_05": { "templateId": "Quest:Quest_BR_LevelUp_SeasonLevel_ProgressiveB_05", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134882,7 +134882,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134895,7 +134895,7 @@ "S4-Quest:Quest_BR_Damage": { "templateId": "Quest:Quest_BR_Damage", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134906,7 +134906,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134919,7 +134919,7 @@ "S4-Quest:Quest_BR_Land_Locations_Different": { "templateId": "Quest:Quest_BR_Land_Locations_Different", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134930,7 +134930,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134962,7 +134962,7 @@ "S4-Quest:Quest_BR_Outlive": { "templateId": "Quest:Quest_BR_Outlive", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134973,7 +134973,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -134986,7 +134986,7 @@ "S4-Quest:Quest_BR_Place_Win": { "templateId": "Quest:Quest_BR_Place_Win", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -134997,7 +134997,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -135010,7 +135010,7 @@ "S4-Quest:Quest_BR_Play": { "templateId": "Quest:Quest_BR_Play", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -135021,7 +135021,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -135034,7 +135034,7 @@ "S4-Quest:Quest_BR_Play_Min1Elimination": { "templateId": "Quest:Quest_BR_Play_Min1Elimination", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -135045,7 +135045,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -135058,7 +135058,7 @@ "S4-Quest:Quest_BR_Play_Min1Friend": { "templateId": "Quest:Quest_BR_Play_Min1Friend", "attributes": { - "creation_time": "2024-05-31T19:57:34.941Z", + "creation_time": "2024-06-24T20:17:00.091Z", "level": -1, "item_seen": true, "playlists": [], @@ -135069,7 +135069,7 @@ "quest_pool": "", "quest_state": "Active", "bucket": "", - "last_state_change_time": "2024-05-31T19:57:34.941Z", + "last_state_change_time": "2024-06-24T20:17:00.091Z", "challenge_linked_quest_parent": "", "max_level_bonus": 0, "xp": 0, @@ -135078,6 +135078,30 @@ "completion_battlepass_athena_friend": 0 }, "quantity": 1 + }, + "b1b4d9c3-4a9b-4916-a15f-0eaf8b24e472": { + "templateId": "Quest:AthenaDailyQuest_PlayerEliminationSniperRifles", + "attributes": { + "creation_time": "2024-06-24T20:17:00.086Z", + "level": -1, + "item_seen": false, + "playlists": [], + "sent_new_notification": true, + "challenge_bundle_id": "", + "xp_reward_scalar": 1, + "challenge_linked_quest_given": "", + "quest_pool": "", + "quest_state": "Active", + "bucket": "", + "last_state_change_time": "2024-06-24T20:17:00.086Z", + "challenge_linked_quest_parent": "", + "max_level_bonus": 0, + "xp": 0, + "quest_rarity": "uncommon", + "favorite": false, + "completion_athena_daily_kill_players_sniper_v2": 0 + }, + "quantity": 1 } }, "stats": { @@ -135090,7 +135114,7 @@ "favorite_victorypose": "", "mfa_reward_claimed": true, "quest_manager": { - "dailyLoginInterval": "2024-05-31T19:09:53.495Z", + "dailyLoginInterval": "2024-06-24T20:17:00.086Z", "dailyQuestRerolls": 1 }, "book_level": 1, @@ -135148,5 +135172,5 @@ ] } }, - "commandRevision": 20 + "commandRevision": 22 } \ No newline at end of file diff --git a/backend/responses/friendslist.json b/backend/responses/friendslist.json index 4e3682a..0307a74 100644 --- a/backend/responses/friendslist.json +++ b/backend/responses/friendslist.json @@ -75,5 +75,12 @@ "direction": "OUTBOUND", "created": "2024-05-31T19:50:04.738Z", "favorite": false + }, + { + "accountId": "Player724", + "status": "ACCEPTED", + "direction": "OUTBOUND", + "created": "2024-06-24T20:15:48.062Z", + "favorite": false } ] \ No newline at end of file diff --git a/backend/responses/friendslist2.json b/backend/responses/friendslist2.json index f7e36d3..19e0de1 100644 --- a/backend/responses/friendslist2.json +++ b/backend/responses/friendslist2.json @@ -98,6 +98,15 @@ "note": "", "favorite": false, "created": "2024-05-31T19:50:04.738Z" + }, + { + "accountId": "Player724", + "groups": [], + "mutual": 0, + "alias": "", + "note": "", + "favorite": false, + "created": "2024-06-24T20:15:48.062Z" } ], "incoming": [], diff --git a/common/lib/common.dart b/common/lib/common.dart index 4ca9b0d..89662b6 100644 --- a/common/lib/common.dart +++ b/common/lib/common.dart @@ -10,6 +10,7 @@ export 'package:reboot_common/src/model/server_result.dart'; export 'package:reboot_common/src/model/server_type.dart'; export 'package:reboot_common/src/model/update_status.dart'; export 'package:reboot_common/src/model/update_timer.dart'; +export 'package:reboot_common/src/model/fortnite_server.dart'; export 'package:reboot_common/src/model/dll.dart'; export 'package:reboot_common/src/util/backend.dart'; export 'package:reboot_common/src/util/build.dart'; @@ -17,4 +18,5 @@ export 'package:reboot_common/src/util/dll.dart'; export 'package:reboot_common/src/util/network.dart'; export 'package:reboot_common/src/util/patcher.dart'; export 'package:reboot_common/src/util/path.dart'; -export 'package:reboot_common/src/util/process.dart'; \ No newline at end of file +export 'package:reboot_common/src/util/process.dart'; +export 'package:reboot_common/src/util/log.dart'; \ No newline at end of file diff --git a/common/lib/src/constant/backend.dart b/common/lib/src/constant/backend.dart index 8a2a1be..78449d6 100644 --- a/common/lib/src/constant/backend.dart +++ b/common/lib/src/constant/backend.dart @@ -1,2 +1,3 @@ const String kDefaultBackendHost = "127.0.0.1"; -const int kDefaultBackendPort = 3551; \ No newline at end of file +const int kDefaultBackendPort = 3551; +const int kDefaultXmppPort = 80; \ No newline at end of file diff --git a/common/lib/src/extension/path.dart b/common/lib/src/extension/path.dart index f5fc2fd..3b8ff89 100644 --- a/common/lib/src/extension/path.dart +++ b/common/lib/src/extension/path.dart @@ -5,9 +5,9 @@ import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; extension FortniteVersionExtension on FortniteVersion { - static DateTime _marker = DateTime.fromMicrosecondsSinceEpoch(0); + static String _marker = "FortniteClient.mod"; - static File? findExecutable(Directory directory, String name) { + static File? findFile(Directory directory, String name) { try{ final result = directory.listSync(recursive: true) .firstWhere((element) => path.basename(element.path) == name); @@ -18,39 +18,24 @@ extension FortniteVersionExtension on FortniteVersion { } Future get shippingExecutable async { - final result = findExecutable(location, "FortniteClient-Win64-Shipping.exe"); + final result = findFile(location, "FortniteClient-Win64-Shipping.exe"); if(result == null) { return null; } - final lastModified = await _getLastModifiedTime(result); - if(lastModified != _marker) { - await Isolate.run(() => patchHeadless(result)); - await _setLastModifiedTime(result); + final marker = findFile(location, _marker); + if(marker != null) { + return result; } + await Isolate.run(() => patchHeadless(result)); + await File("${location.path}\\$_marker").create(); return result; } - Future _setLastModifiedTime(File result) async { - try { - await result.setLastModified(_marker); - }catch(_) { - // Ignored - } - } + File? get launcherExecutable => findFile(location, "FortniteLauncher.exe"); - Future _getLastModifiedTime(File result) async { - try { - return await result.lastModified(); - }catch(_) { - return null; - } - } + File? get eacExecutable => findFile(location, "FortniteClient-Win64-Shipping_EAC.exe"); - File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe"); - - File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe"); - - File? get splashBitmap => findExecutable(location, "Splash.bmp"); + File? get splashBitmap => findFile(location, "Splash.bmp"); } \ No newline at end of file diff --git a/common/lib/src/model/fortnite_build.dart b/common/lib/src/model/fortnite_build.dart index 268695f..50248d6 100644 --- a/common/lib/src/model/fortnite_build.dart +++ b/common/lib/src/model/fortnite_build.dart @@ -1,15 +1,17 @@ import 'dart:io'; import 'dart:isolate'; +import 'package:version/version.dart'; + class FortniteBuild { - final String identifier; - final String version; + final Version version; final String link; + final bool available; FortniteBuild({ - required this.identifier, required this.version, - required this.link + required this.link, + required this.available }); } diff --git a/common/lib/src/model/fortnite_server.dart b/common/lib/src/model/fortnite_server.dart new file mode 100644 index 0000000..4084e1c --- /dev/null +++ b/common/lib/src/model/fortnite_server.dart @@ -0,0 +1,47 @@ +class FortniteServer { + final String id; + final String name; + final String description; + final String author; + final String ip; + final String version; + final String? password; + final DateTime timestamp; + final bool discoverable; + + FortniteServer({ + required this.id, + required this.name, + required this.description, + required this.author, + required this.ip, + required this.version, + required this.password, + required this.timestamp, + required this.discoverable + }); + + factory FortniteServer.fromJson(json) => FortniteServer( + id: json["id"], + name: json["name"], + description: json["description"], + author: json["author"], + ip: json["ip"], + version: json["version"], + password: json["password"], + timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now(), + discoverable: json["discoverable"] ?? false + ); + + Map toJson() => { + "id": id, + "name": name, + "description": description, + "author": author, + "ip": ip, + "version": version, + "password": password, + "timestamp": timestamp.toString(), + "discoverable": discoverable + }; +} diff --git a/common/lib/src/model/fortnite_version.dart b/common/lib/src/model/fortnite_version.dart index 92586fc..65a4847 100644 --- a/common/lib/src/model/fortnite_version.dart +++ b/common/lib/src/model/fortnite_version.dart @@ -1,17 +1,22 @@ import 'dart:io'; +import 'package:version/version.dart'; + class FortniteVersion { - String name; + Version content; Directory location; FortniteVersion.fromJson(json) - : name = json["name"], + : content = Version.parse(json["content"]), location = Directory(json["location"]); - FortniteVersion({required this.name, required this.location}); + FortniteVersion({required this.content, required this.location}); Map toJson() => { - 'name': name, + 'content': content.toString(), 'location': location.path }; + + @override + bool operator ==(Object other) => other is FortniteVersion && this.content == other.content; } \ No newline at end of file diff --git a/common/lib/src/model/game_instance.dart b/common/lib/src/model/game_instance.dart index 0853a4e..709b88b 100644 --- a/common/lib/src/model/game_instance.dart +++ b/common/lib/src/model/game_instance.dart @@ -13,6 +13,7 @@ class GameInstance { bool launched; bool movedToVirtualDesktop; bool tokenError; + bool killed; GameInstance? child; GameInstance({ @@ -22,9 +23,19 @@ class GameInstance { required this.eacPid, required this.serverType, required this.child - }): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = []; + }): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = []; void kill() { + GameInstance? child = this; + while(child != null) { + child._kill(); + child = child.child; + } + } + + void _kill() { + launched = true; + killed = true; Process.killPid(gamePid, ProcessSignal.sigabrt); if(launcherPid != null) { Process.killPid(launcherPid!, ProcessSignal.sigabrt); @@ -33,19 +44,6 @@ class GameInstance { Process.killPid(eacPid!, ProcessSignal.sigabrt); } } - - bool get nestedHosting { - GameInstance? child = this; - while(child != null) { - if(child.serverType != null) { - return true; - } - - child = child.child; - } - - return false; - } } enum GameServerType { diff --git a/common/lib/src/model/server_result.dart b/common/lib/src/model/server_result.dart index 509c7e3..40063f7 100644 --- a/common/lib/src/model/server_result.dart +++ b/common/lib/src/model/server_result.dart @@ -4,6 +4,11 @@ class ServerResult { final StackTrace? stackTrace; ServerResult(this.type, {this.error, this.stackTrace}); + + @override + String toString() { + return 'ServerResult{type: $type, error: $error, stackTrace: $stackTrace}'; + } } enum ServerResultType { @@ -21,7 +26,8 @@ enum ServerResultType { freePortError, pingingRemote, pingingLocal, - pingError; + pingError, + processError; bool get isError => name.contains("Error"); diff --git a/common/lib/src/util/backend.dart b/common/lib/src/util/backend.dart index ef954b2..587961d 100644 --- a/common/lib/src/util/backend.dart +++ b/common/lib/src/util/backend.dart @@ -26,6 +26,7 @@ Future isBackendPortFree() async => await pingBackend(kDefaultBackendHost, Future freeBackendPort() async { await killProcessByPort(kDefaultBackendPort); + await killProcessByPort(kDefaultXmppPort); final standardResult = await isBackendPortFree(); if(standardResult) { return true; @@ -35,21 +36,24 @@ Future freeBackendPort() async { } Future pingBackend(String host, int port, [bool https=false]) async { - var hostName = host.replaceFirst("http://", "").replaceFirst("https://", ""); - var declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; + final hostName = host.replaceFirst("http://", "").replaceFirst("https://", ""); + final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; try{ - var uri = Uri( + final uri = Uri( scheme: declaredScheme ?? (https ? "https" : "http"), host: hostName, port: port, path: "unknown" ); - var client = HttpClient() - ..connectionTimeout = const Duration(seconds: 5); - var request = await client.getUrl(uri); - var response = await request.close(); - return response.statusCode == 200 || response.statusCode == 404 ? uri : null; - }catch(_){ + log("[BACKEND] Pinging $uri..."); + final client = HttpClient() + ..connectionTimeout = const Duration(seconds: 10); + final request = await client.getUrl(uri); + await request.close().timeout(const Duration(seconds: 10)); + log("[BACKEND] Ping successful"); + return uri; + }catch(error){ + log("[BACKEND] Cannot ping backend: $error"); return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true); } } @@ -59,16 +63,16 @@ Stream watchMatchmakingIp() async* { return; } - var observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify); + final 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"); + final config = Config.fromString(await matchmakerConfigFile.readAsString()); + final ip = config.get("GameServer", "ip"); if(ip == null) { return null; } - var port = config.get("GameServer", "port"); + final port = config.get("GameServer", "port"); if(port == null) { return null; } @@ -89,14 +93,14 @@ Stream watchMatchmakingIp() async* { } Future writeMatchmakingIp(String text) async { - var exists = await matchmakerConfigFile.exists(); + final exists = await matchmakerConfigFile.exists(); if(!exists) { return; } _semaphore.acquire(); - var splitIndex = text.indexOf(":"); - var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text; + final splitIndex = text.indexOf(":"); + final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text; var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort; if(port.isBlank) { port = kDefaultGameServerPort; @@ -104,7 +108,7 @@ Future writeMatchmakingIp(String text) async { _lastIp = ip; _lastPort = port; - var config = Config.fromString(await matchmakerConfigFile.readAsString()); + final config = Config.fromString(await matchmakerConfigFile.readAsString()); config.set("GameServer", "ip", ip); config.set("GameServer", "port", port); await matchmakerConfigFile.writeAsString(config.toString(), flush: true); diff --git a/common/lib/src/util/build.dart b/common/lib/src/util/build.dart index 433c245..390c137 100644 --- a/common/lib/src/util/build.dart +++ b/common/lib/src/util/build.dart @@ -8,6 +8,7 @@ import 'package:dio/io.dart'; import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; import 'package:reboot_common/src/extension/types.dart'; +import 'package:version/version.dart'; const String kStopBuildDownloadSignal = "kill"; @@ -23,7 +24,7 @@ Dio _buildDioInstance() { return dio; } -final String _archiveSourceUrl = "http://185.203.216.3/versions.json"; +final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md"; final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download"; const String _unavailableError = "The build downloader is not available right now"; @@ -41,15 +42,35 @@ Future> fetchBuilds(ignored) async { return []; } - final data = jsonDecode(response.data ?? "{}"); var results = []; - for(final entry in data.entries) { - results.add(FortniteBuild( - identifier: entry.key, - version: "${entry.value["title"]} (${entry.key})", - link: entry.value["url"] - )); + for (final line in response.data?.split("\n") ?? []) { + if (!line.startsWith("|")) { + continue; + } + + var parts = line.substring(1, line.length - 1).split("|"); + if (parts.isEmpty) { + continue; + } + + var versionName = parts.first.trim(); + final separator = versionName.indexOf("-"); + if(separator != -1) { + versionName = versionName.substring(0, separator); + } + + final link = parts.last.trim(); + try { + results.add(FortniteBuild( + version: Version.parse(versionName), + link: link, + available: link.endsWith(".zip") || link.endsWith(".rar") + )); + } on FormatException { + // Ignore + } } + return results; } diff --git a/gui/lib/src/util/log.dart b/common/lib/src/util/log.dart similarity index 71% rename from gui/lib/src/util/log.dart rename to common/lib/src/util/log.dart index 8ddffc4..9b5f9ef 100644 --- a/gui/lib/src/util/log.dart +++ b/common/lib/src/util/log.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:reboot_common/common.dart'; import 'package:sync/semaphore.dart'; -final File _loggingFile = _createLoggingFile(); +final File launcherLogFile = _createLoggingFile(); final Semaphore _semaphore = Semaphore(1); File _createLoggingFile() { @@ -20,9 +20,9 @@ void log(String message) async { try { await _semaphore.acquire(); print(message); - await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true); + await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true); }catch(error) { - print(error); + print("[LOGGER_ERROR] An error occurred while logging: $error"); }finally { _semaphore.release(); } diff --git a/common/lib/src/util/path.dart b/common/lib/src/util/path.dart index 4551fdc..1f9da61 100644 --- a/common/lib/src/util/path.dart +++ b/common/lib/src/util/path.dart @@ -6,7 +6,7 @@ Directory get installationDirectory => Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls"); Directory get assetsDirectory { - var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets"); + final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets"); if(directory.existsSync()) { return directory; } diff --git a/common/lib/src/util/process.dart b/common/lib/src/util/process.dart index 77fccb1..1983581 100644 --- a/common/lib/src/util/process.dart +++ b/common/lib/src/util/process.dart @@ -104,7 +104,7 @@ Future startElevatedProcess({required String executable, required String a return shellResult == 1; } -Future startProcess({required File executable, List? args, bool useTempBatch = true, bool window = false, String? name}) async { +Future startProcess({required File executable, List? args, bool useTempBatch = true, bool window = false, String? name, Map? environment}) async { final argsOrEmpty = args ?? []; if(useTempBatch) { final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process"); @@ -115,6 +115,7 @@ Future startProcess({required File executable, List? args, bool tempScriptFile.path, [], workingDirectory: executable.parent.path, + environment: environment, mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal, runInShell: window ); @@ -202,6 +203,7 @@ Future watchProcess(int pid) async { return await completer.future; } +// TODO: Template List createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) { if(password.isEmpty) { username = '${_parseUsername(username, host)}@projectreboot.dev'; diff --git a/common/pubspec.yaml b/common/pubspec.yaml index 4f7601b..1fff3ba 100644 --- a/common/pubspec.yaml +++ b/common/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: sync: ^0.3.0 uuid: ^3.0.6 shelf_web_socket: ^2.0.0 + version: ^3.0.2 dev_dependencies: flutter_lints: ^2.0.1 \ No newline at end of file diff --git a/gui/assets/backend/lawinserver.exe b/gui/assets/backend/lawinserver.exe index a8bf28dc29b7a5039ae068505ced738193586be7..39581f50940edc9d0aacbba71806fa8a0c652404 100644 GIT binary patch delta 559316 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%0D`X`V9Pv4+hd2!Z4 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 Vu+{UQp8KwMz3b`EAK5eS{{iuR$oBvM delta 17135 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 V%PL0PwPM8o{onu1oN305{D0~1LBRk3 diff --git a/gui/assets/info/en/faq/1. What is Project Reboot b/gui/assets/info/en/faq/1. What is Project Reboot deleted file mode 100644 index ab32a68..0000000 --- a/gui/assets/info/en/faq/1. What is Project Reboot +++ /dev/null @@ -1,3 +0,0 @@ -Project Reboot is a game server for Fortnite that aims to support as many seasons as possible. -The project was started on Discord by Milxnor, while the launcher is developed by Auties00. -Both are open source on GitHub, anyone can easily contribute or audit the code!" \ No newline at end of file diff --git a/gui/assets/info/en/faq/10. Corrupted build b/gui/assets/info/en/faq/10. Corrupted build deleted file mode 100644 index 7ecf66b..0000000 --- a/gui/assets/info/en/faq/10. Corrupted build +++ /dev/null @@ -1 +0,0 @@ -Your version of Fortnite is corrupted, download it again from the launcher, or use another build. \ No newline at end of file diff --git a/gui/assets/info/en/faq/11. LawinV2 and backends with email and password authentication b/gui/assets/info/en/faq/11. LawinV2 and backends with email and password authentication deleted file mode 100644 index 045d357..0000000 --- a/gui/assets/info/en/faq/11. LawinV2 and backends with email and password authentication +++ /dev/null @@ -1,3 +0,0 @@ -Support for LawinV2 is available in the launcher. -To use the backend, select a local or remote backend from the "Backend" tab in the launcher. -To use the credentials, click on the avatar on the top left of the launcher and enter your email and password \ No newline at end of file diff --git a/gui/assets/info/en/faq/12. Can I get skins in game b/gui/assets/info/en/faq/12. Can I get skins in game deleted file mode 100644 index 66ec1f4..0000000 --- a/gui/assets/info/en/faq/12. Can I get skins in game +++ /dev/null @@ -1,2 +0,0 @@ -No, skins don't work in Reboot. -This is because Epic asked us to remove them. \ No newline at end of file diff --git a/gui/assets/info/en/faq/2. What is a Fortnite game server b/gui/assets/info/en/faq/2. What is a Fortnite game server deleted file mode 100644 index ec5bf41..0000000 --- a/gui/assets/info/en/faq/2. What is a Fortnite game server +++ /dev/null @@ -1,7 +0,0 @@ -If you have ever played Minecraft multiplayer, you might know that the servers you join are hosted on a computer running a program, called Minecraft Game Server. -While the Minecraft Game server is written by the creators of Minecraft, Mojang, Epic Games doesn't provide an equivalent for Fortnite. -By exploiting the Fortnite internals, though, it's possible to create a game server just like in Minecraft: this is in easy terms what Project Reboot does. -Some Fortnite versions support running this game server in the background without rendering the game("headless"), while others still require the full game to be open. -Just like in Minecraft, you need a game client to play the game and one to host the server. -By default, a game server is automatically started on your PC when you start a Fortnite version from the "Play" section in the launcher. -If you want to play in another way, for example by joining a server hosted by one of your friends instead of running one yourself, you can checkout the "Multiplayer" section in the "Play" tab of the launcher. \ No newline at end of file diff --git a/gui/assets/info/en/faq/3. Types of Fortnite game server b/gui/assets/info/en/faq/3. Types of Fortnite game server deleted file mode 100644 index 73f672d..0000000 --- a/gui/assets/info/en/faq/3. Types of Fortnite game server +++ /dev/null @@ -1,4 +0,0 @@ -Some Fortnite versions support running this game server in the background without rendering the game: this type of server is called "headless" as the game is running, but you can't see it on your screen. -If headless is not supported by the Fortnite version you want to play, or if you disabled it manually from the "Configuration" section in the "Host" tab of the launcher, you will see an instance of Fortnite open on your screen. -For convenience, this window will be opened on a new Virtual Desktop, if your Windows version supports it. This feature can be disabled as well from from the "Configuration" section in the "Host" tab of the launcher. -Just like in Minecraft, you need a game client to play the game and one to host the server. \ No newline at end of file diff --git a/gui/assets/info/en/faq/4. How can others join my game server b/gui/assets/info/en/faq/4. How can others join my game server deleted file mode 100644 index ecba1ed..0000000 --- a/gui/assets/info/en/faq/4. How can others join my game server +++ /dev/null @@ -1,22 +0,0 @@ -For others to join your game server, port 7777 must be accessible on your PC. -One option is to use a private VPN service like Hamachi or Radmin, but all of the players will need to download this software. -The best solution is to use port forwarding: -1. Set a static IP - If you don't have already a static IP set, set one by following any tutorial on Google -2. Log into your router's admin panel - Usually this can be accessed on any web browser by going to http://192.168.1.1/ - You might need a username and a password to log in: refer to your router's manual for precise instructions -3. Find the port forwarding section - Once logged in into the admin panel, navigate to the port forwarding section of your router's settings - This location may vary from router to router, but it's typically labelled as "Port Forwarding," "Port Mapping" or "Virtual Server" - Refer to your router's manual for precise instructions -4. Add a port forwarding rule - Now, you'll need to create a new port forwarding rule. Here's what you'll typically need to specify: - - Service Name: Choose a name for your port forwarding rule (e.g., "Fortnite Game Server") - - Port Number: Enter 7777 for both the external and internal ports - - Protocol: Select the UDP protocol - - Internal IP Address: Enter the static IP address you set earlier - - Enable: Make sure the port forwarding rule is enabled -5. Save and apply the changes - After configuring the port forwarding rule, save your changes and apply them - This step may involve clicking a "Save" or "Apply" button on your router's web interface \ No newline at end of file diff --git a/gui/assets/info/en/faq/5. What is a backend b/gui/assets/info/en/faq/5. What is a backend deleted file mode 100644 index 6d1df79..0000000 --- a/gui/assets/info/en/faq/5. What is a backend +++ /dev/null @@ -1,6 +0,0 @@ -A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features. -By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github. -If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue. -LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend. -Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user. -You can run these alternatives either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher. \ No newline at end of file diff --git a/gui/assets/info/en/faq/6. What is the Unreal Engine console b/gui/assets/info/en/faq/6. What is the Unreal Engine console deleted file mode 100644 index 111cc79..0000000 --- a/gui/assets/info/en/faq/6. What is the Unreal Engine console +++ /dev/null @@ -1,4 +0,0 @@ -Many Fortnite versions don't support entering in game by clicking the \"Play\" button. -Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1 -Keep in mind that the Unreal Engine console key is controlled by the backend, so this is true only if you are using the embedded backend: custom backends might use different keys. -When using the embedded backend, you can customize the key used to open the console in the \"Backend\" tab of the Reboot Launcher. \ No newline at end of file diff --git a/gui/assets/info/en/faq/7. I cannot open Fortnite because of an authentication error b/gui/assets/info/en/faq/7. I cannot open Fortnite because of an authentication error deleted file mode 100644 index 39ebada..0000000 --- a/gui/assets/info/en/faq/7. I cannot open Fortnite because of an authentication error +++ /dev/null @@ -1,4 +0,0 @@ -To resolve this issue: -- Check that your backend is working correctly from the "Backend" tab -- If you are using a custom backend, try to use the embedded one -- Try to run the backend as detached by enabling the "Detached" option in the "Backend" tab \ No newline at end of file diff --git a/gui/assets/info/en/faq/8. Why do I see two Fortnite versions opened on my PC b/gui/assets/info/en/faq/8. Why do I see two Fortnite versions opened on my PC deleted file mode 100644 index 7201bff..0000000 --- a/gui/assets/info/en/faq/8. Why do I see two Fortnite versions opened on my PC +++ /dev/null @@ -1,2 +0,0 @@ -As explained in the "What is a Fortnite game server?" section, one instance of Fortnite is used to host the game server, while the other is used to let you play. -The Fortnite instance used up by the game server is usually frozen, so it should be hard to use the wrong one to try to play. \ No newline at end of file diff --git a/gui/assets/info/en/faq/9. I cannot enter in a match when I'm in Fortnite b/gui/assets/info/en/faq/9. I cannot enter in a match when I'm in Fortnite deleted file mode 100644 index f98b80e..0000000 --- a/gui/assets/info/en/faq/9. I cannot enter in a match when I'm in Fortnite +++ /dev/null @@ -1,3 +0,0 @@ -As explained in the "What is the Unreal Engine console?" section, the "Play" button doesn't work in many Fortnite versions. -Instead, you need to click the key assigned to the Unreal Engine console, by default F8 or the tilde(the button above tab), and type open 127.0.0.1 -If that doesn't work, go to the Host page and make sure that you are running a game server by clicking "Start Hosting". \ No newline at end of file diff --git a/gui/assets/info/en/questions/1. What is Project Reboot b/gui/assets/info/en/questions/1. What is Project Reboot deleted file mode 100644 index ae2e2a4..0000000 --- a/gui/assets/info/en/questions/1. What is Project Reboot +++ /dev/null @@ -1,3 +0,0 @@ -A Fortnite game server created by Milxnor -A Minecraft game server created by Chief Keef -I don't know \ No newline at end of file diff --git a/gui/assets/info/en/questions/10. Which of these is a bug that you should report on Discord b/gui/assets/info/en/questions/10. Which of these is a bug that you should report on Discord deleted file mode 100644 index b0354bb..0000000 --- a/gui/assets/info/en/questions/10. Which of these is a bug that you should report on Discord +++ /dev/null @@ -1,3 +0,0 @@ -A version I downloaded from the launcher crashes -A version of Fortnite I downloaded from the internet crashes -I can't enter in game from the lobby \ No newline at end of file diff --git a/gui/assets/info/en/questions/11. I can't enter in game from the Fortnite lobby b/gui/assets/info/en/questions/11. I can't enter in game from the Fortnite lobby deleted file mode 100644 index 7d7c6a5..0000000 --- a/gui/assets/info/en/questions/11. I can't enter in game from the Fortnite lobby +++ /dev/null @@ -1,3 +0,0 @@ -Click the Unreal Engine Key(F8 or the tilde by default) and type open 127.0.0.1 -Report a bug on Discord because there is a clearly a bug -Cry \ No newline at end of file diff --git a/gui/assets/info/en/questions/12. If you get an authentication error, what should you do b/gui/assets/info/en/questions/12. If you get an authentication error, what should you do deleted file mode 100644 index 180f58b..0000000 --- a/gui/assets/info/en/questions/12. If you get an authentication error, what should you do +++ /dev/null @@ -1,3 +0,0 @@ -Switch my backend to embedded and try again before reporting a bug on Discord -Report a bug on Discord immediately and flood the chat -Cry \ No newline at end of file diff --git a/gui/assets/info/en/questions/13. Can I have skins in-game b/gui/assets/info/en/questions/13. Can I have skins in-game deleted file mode 100644 index 3f91cac..0000000 --- a/gui/assets/info/en/questions/13. Can I have skins in-game +++ /dev/null @@ -1,3 +0,0 @@ -No, it's not possible -Yes, I just have to send Auties a message -Yes, let me ask on Discord how to get them \ No newline at end of file diff --git a/gui/assets/info/en/questions/2. Which seasons are supported b/gui/assets/info/en/questions/2. Which seasons are supported deleted file mode 100644 index b73604f..0000000 --- a/gui/assets/info/en/questions/2. Which seasons are supported +++ /dev/null @@ -1,3 +0,0 @@ -Only the ones I can download from the launcher -Any season between season 0 and 15 works -Depends on the day of the week \ No newline at end of file diff --git a/gui/assets/info/en/questions/3. What is 127.0.0.1 b/gui/assets/info/en/questions/3. What is 127.0.0.1 deleted file mode 100644 index 9627e4e..0000000 --- a/gui/assets/info/en/questions/3. What is 127.0.0.1 +++ /dev/null @@ -1,3 +0,0 @@ -The address of your local machine where the game server will be hosted -Playboi Carti's hiding spot -I don't know \ No newline at end of file diff --git a/gui/assets/info/en/questions/4. What is a Fortnite game server b/gui/assets/info/en/questions/4. What is a Fortnite game server deleted file mode 100644 index f5b0d8c..0000000 --- a/gui/assets/info/en/questions/4. What is a Fortnite game server +++ /dev/null @@ -1,3 +0,0 @@ -A Fortnite window used to host matches instead of playing the game -A standalone piece of software to host matches -A stolen Epic games server we got by paying Tim Apple \ No newline at end of file diff --git a/gui/assets/info/en/questions/5. Can I play if I haven't started a game server b/gui/assets/info/en/questions/5. Can I play if I haven't started a game server deleted file mode 100644 index d9b16ce..0000000 --- a/gui/assets/info/en/questions/5. Can I play if I haven't started a game server +++ /dev/null @@ -1,3 +0,0 @@ -No, I will be kicked back to the lobby, unless I've joined someone else's server -Yes, one will be started automatically as soon as I click play -Yes, it's not needed to play \ No newline at end of file diff --git a/gui/assets/info/en/questions/6. What is an headless game server b/gui/assets/info/en/questions/6. What is an headless game server deleted file mode 100644 index 32d24f0..0000000 --- a/gui/assets/info/en/questions/6. What is an headless game server +++ /dev/null @@ -1,3 +0,0 @@ -A game server that doesn't render the Fortnite window to save memory -A game server hosted on someone else's PC -A game server running in Milxnor's mind \ No newline at end of file diff --git a/gui/assets/info/en/questions/7. Why do I see two Fortnite games when I play b/gui/assets/info/en/questions/7. Why do I see two Fortnite games when I play deleted file mode 100644 index 15776aa..0000000 --- a/gui/assets/info/en/questions/7. Why do I see two Fortnite games when I play +++ /dev/null @@ -1,3 +0,0 @@ -Some seasons don't support a headless server, so the other frozen Fortnite is the game server -The launcher is bugged: I must report a bug on Discord -Auties hardcoded a crypto miner in Fortnite: by opening two games he gets more cash \ No newline at end of file diff --git a/gui/assets/info/en/questions/8. How can other players join my game b/gui/assets/info/en/questions/8. How can other players join my game deleted file mode 100644 index d91d6c2..0000000 --- a/gui/assets/info/en/questions/8. How can other players join my game +++ /dev/null @@ -1,3 +0,0 @@ -I can open port 7777 on my router or use a private VPN service -I don't have to do anything, it's all automatic -What are you talking about? \ No newline at end of file diff --git a/gui/assets/info/en/questions/9. What is a backend b/gui/assets/info/en/questions/9. What is a backend deleted file mode 100644 index efb1a4e..0000000 --- a/gui/assets/info/en/questions/9. What is a backend +++ /dev/null @@ -1,3 +0,0 @@ -A piece of software to emulate Epic Games' servers for authentication -A piece of software that makes the launcher work correctly -I don't know \ No newline at end of file diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index dd6e4f8..a8a9e22 100644 --- a/gui/lib/l10n/reboot_en.arb +++ b/gui/lib/l10n/reboot_en.arb @@ -129,7 +129,7 @@ "importVersionDescription": "Import a new version of Fortnite into the launcher", "addLocalBuildName": "Add a version from this PC's local storage", "addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work", - "addLocalBuildContent": "Add local build", + "addVersion": "Add version", "downloadBuildName": "Download any version from the cloud", "downloadBuildDescription": "Download any Fortnite build easily from the cloud", "downloadBuildContent": "Download build", @@ -151,8 +151,10 @@ "downloadDllError": "An error occurred while downloading {name}: {error}", "downloadDllRetry": "Retry", "uncaughtErrorMessage": "An uncaught error was thrown: {error}", - "launchingHeadlessServer": "Launching the game server...", - "launchingGameClient": "Launching the game client...", + "launchingGameServer": "Launching the game server...", + "launchingGameClientOnly": "Launching the game client without a server...", + "launchingGameClientAndServer": "Launching the game client and server...", + "startGameServer": "Start a game server", "usernameOrEmail": "Username/Email", "usernameOrEmailPlaceholder": "Type your username or email", "password": "Password", @@ -167,16 +169,16 @@ "stoppingServer": "Stopping the backend...", "stoppedServer": "The backend was stopped successfully", "stopServerError": "An error occurred while stopping the backend: {error}", - "missingHostNameError": "Missing hostname in the {name} configuration", + "missingHostNameError": "Missing hostname in the backend configuration", "missingPortError": "Missing port in the backend configuration", "illegalPortError": "Invalid port in the backend configuration", "freeingPort": "Freeing the backend port...", "freedPort": "The backend port was freed successfully", "freePortError": "An error occurred while freeing the backend port: {error}", - "pingingRemoteServer": "Pinging the remote backend...", - "pingingLocalServer": "Pinging the {type} backend...", + "pingingServer": "Pinging the {type} backend...", "pingError": "Cannot ping the {type} backend", "joinSelfServer": "You can't join your own server", + "cannotJoinServerVersion": "You can't join this server: download Fortnite {version}", "wrongServerPassword": "Wrong password: please try again", "offlineServer": "This server isn't online right now: please try again later", "serverPassword": "Password", @@ -192,12 +194,12 @@ "deleteVersionCancel": "Keep", "deleteVersionConfirm": "Delete", "versionName": "Name", - "versionNameLabel": "Type the new version name", + "versionNameLabel": "Type the version name", "newVersionNameConfirm": "Save", - "newVersionNameLabel": "Type the new version name", - "gameFolderTitle": "Game folder", - "gameFolderPlaceholder": "Type the new game folder", - "gameFolderPlaceWindowTitle": "Select game folder", + "newVersionNameLabel": "Type the version name", + "gameFolderTitle": "Game directory", + "gameFolderPlaceholder": "Type the game directory", + "gameFolderPlaceWindowTitle": "Select game directory", "gameFolderLabel": "Path", "openInExplorer": "Open in explorer", "modify": "Modify", @@ -220,7 +222,7 @@ "buildInstallationDirectoryWindowTitle": "Select installation directory", "timeLeft": "Time left: {timeLeft, plural, =0{less than a minute} =1{about {timeLeft} minute} other{about {timeLeft} minutes}}", "localBuildsWarning": "Local builds are not guaranteed to work", - "saveLocalVersion": "Save", + "saveLocalVersion": "Add", "embedded": "Embedded", "remote": "Remote", "local": "Local", @@ -244,7 +246,7 @@ "versionAlreadyExists": "This version already exists", "emptyGamePath": "Empty game path", "directoryDoesNotExist": "Directory doesn't exist", - "missingShippingExe": "Invalid game path: missing FortniteClient-Win64-Shipping", + "missingShippingExe": "Invalid game path: missing Fortnite executable", "invalidDownloadPath": "Invalid download path", "invalidDllPath": "Invalid dll path", "dllDoesNotExist": "The file doesn't exist", @@ -256,9 +258,9 @@ "missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted", "corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version", "corruptedDllError": "Cannot inject dll: {error}", + "missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings", "tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})", "unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}", - "serverNoLongerAvailable": "{owner}'s server is no longer available", "serverNoLongerAvailableUnnamed": "The previous server is no longer available", "noServerFound": "No server found: invalid or expired link", "settingsUtilsThemeName": "Theme", @@ -268,8 +270,6 @@ "system": "System", "settingsUtilsLanguageName": "Language", "settingsUtilsLanguageDescription": "Select the language to use inside the launcher", - "playAutomaticServerName": "Embedded game server", - "playAutomaticServerDescription": "Whether a game server should be started automatically if none was configured", "infoDocumentationName": "Documentation", "infoDocumentationDescription": "Read some tutorials on how to use Reboot", "infoDocumentationContent": "Open GitHub", @@ -277,8 +277,8 @@ "infoDiscordDescription": "Join the discord server to receive help", "infoDiscordContent": "Open Discord", "infoVideoName": "Tutorial", - "infoVideoDescription": "Watch a tutorial to understand how to use the launcher", - "infoVideoContent": "Open YouTube", + "infoVideoDescription": "Show the tutorial again in the launcher", + "infoVideoContent": "Start Tutorial", "dllDeletedTitle": "A critical dll was deleted. If you didn't delete it, your Antivirus probably flagged it. This is a false positive: please disable your Antivirus and try again", "dllDeletedSecondaryAction": "Close", "dllDeletedPrimaryAction": "Try again", @@ -307,5 +307,61 @@ "gameServerTypeDescription": "The type of game server to use", "gameServerTypeHeadless": "Background process", "gameServerTypeVirtualWindow": "Virtual window", - "gameServerTypeWindow": "Normal window" + "gameServerTypeWindow": "Normal window", + "localBuild": "This PC", + "githubArchive": "Cloud archive", + "all": "All", + "accessible": "Accessible", + "playable": "Playable", + "timeDescending": "Time (from newest to oldest)", + "timeAscending": "Time (from oldest to newest)", + "nameAscending": "Name (from A to Z)", + "nameDescending": "Name (from Z to A)", + "none": "none", + "openLog": "Open log", + "backendProcessError": "The backend shut down unexpectedly", + "welcomeTitle": "Welcome to Reboot Launcher", + "welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab", + "welcomeAction": "Take the tour", + "startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.", + "startOnboardingActionLabel": "Let's do it", + "promptPlayPageText": "The Play tab is used to launch the version of Fortnite you want.\nBefore playing, you'll need to host or join a game server.\nYou will learn how to later.", + "promptPlayPageActionLabel": "Next", + "promptPlayVersionText": "Here you can download or import any Fortnite version\nAdd at least one to start using the launcher", + "promptPlayVersionActionLabelHasBuilds": "Next", + "promptPlayVersionActionLabelNoBuilds": "Let's do it", + "promptServerBrowserPageText": "The Server Browser tab is used to find game servers hosted by other players\nServers can be free to join or password protected based on the settings set by the owner", + "promptServerBrowserPageActionLabel": "Next", + "promptHostPageText": "The Host tab is used to host a game server.\nWhen you usually play Fortnite, you connect to an Epic Games' game server.\nTo play using Reboot, you'll need to host the game server yourself, or join someone else's.\nOtherwise, you will be sent back to the lobby when trying to join a game.", + "promptHostPageActionLabel": "Next", + "promptHostInfoText": "This section is used to provide information about your game server for the Server Browser\nIf you don't want other players to join your server, you can skip this part", + "promptHostInfoActionLabelSkip": "Skip", + "promptHostInfoActionLabelConfigure": "Configure", + "promptHostInformationText": "Choose the name for your server", + "promptHostInformationActionLabel": "Next", + "promptHostInformationDescriptionText": "Choose the description for your server", + "promptHostInformationDescriptionActionLabel": "Next", + "promptHostInformationPasswordText": "Set a password for your server, if you need one", + "promptHostInformationPasswordActionLabel": "Next", + "promptHostVersionText": "You can select the version of Fortnite to host here.\nThese are synchronized with the Play tab.", + "promptHostVersionActionLabel": "Next", + "promptHostShareText": "If you don't want to use the server browser, other players can join\nyou server by using your Reboot Launcher link or your public IP.", + "promptHostShareActionLabel": "Next", + "promptBackendPageText": "The Backend tab is used for authentication and queuing.\nWhen you usually play Fortnite, you connect to an Epic Games' backend.\nTo play using Reboot, you'll need to host the backend yourself, or join someone else's.\nIf the backend doesn't work correctly, an authentication error will be displayed.", + "promptBackendPageActionLabel": "Next", + "promptBackendTypePageText": "By default, an embedded LawinV1 backend is started.\nIf you want to run another backend on your PC, like\nLawinV2 or Momentum, select Local. If you want to join,\na backend on someone else's PC, select Remote.", + "promptBackendTypePageActionLabel": "Next", + "promptBackendGameServerAddressText": "When you are using an embedded backend, you can type\nhere the IP of the game server you want to join. When\nyou click Join in the Server Browser, this field will be\nautocompleted. If you are not using an embedded backend,\nyou will need to set the IP manually in your backend configuration.", + "promptBackendGameServerAddressActionLabel": "Next", + "promptBackendUnrealEngineKeyText": "For some Fortnite versions, the PLAY button doesn't work: when this happens,\nyou need to click this Key to open the Unreal Engine console and type: open IP.\nSo for example if you want to join your own server you can type: open 127.0.0.1.\nIf you don't know, 127.0.0.1 is the IP of your local machine. If you are not using\nthe embedded backend, you'll need to set the Unreal Engine key in its configuration.", + "promptBackendUnrealEngineKeyActionLabel": "Next", + "promptBackendDetachedText": "If you get an authentication error when trying to log into Fortnite,\nswitch to embedded backend and enable this option to debug the backend.\nIf you can't fix the error, report a bug on Discord.", + "promptBackendDetachedActionLabel": "Next", + "promptInfoTabText": "The Info tab contains useful links to report bugs and receive support", + "promptInfoTabActionLabel": "Next", + "promptSettingsTabText": "The Settings tab contains options to customize and reset the launcher", + "promptSettingsTabActionLabel": "Done", + "automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!", + "automaticGameServerDialogIgnore": "Ignore", + "automaticGameServerDialogStart": "Start server" } diff --git a/gui/lib/main.dart b/gui/lib/main.dart index c63b7c5..120184b 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -1,10 +1,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:app_links/app_links.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; @@ -14,26 +12,18 @@ import 'package:local_notifier/local_notifier.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; -import 'package:reboot_launcher/src/controller/build_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/controller/update_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/dialog/implementation/error.dart'; -import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/messenger/implementation/error.dart'; +import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/page/implementation/home_page.dart'; -import 'package:reboot_launcher/src/page/implementation/info_page.dart'; -import 'package:reboot_launcher/src/util/log.dart'; -import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/util/translations.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; import 'package:url_protocol/url_protocol.dart'; import 'package:version/version.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:win32/win32.dart'; const double kDefaultWindowWidth = 1164; const double kDefaultWindowHeight = 864; @@ -45,8 +35,8 @@ bool appWithNoStorage = false; void main() { log("[APP] Called"); runZonedGuarded( - () => _startApp(), - (error, stack) => onError(error, stack, false), + () => _startApp(), + (error, stack) => onError(error, stack, false), zoneSpecification: ZoneSpecification( handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) ) @@ -54,6 +44,7 @@ void main() { } Future _startApp() async { + _overrideHttpCertificate(); final errors = []; try { log("[APP] Starting application"); @@ -72,11 +63,6 @@ Future _startApp() async { errors.add(notificationsError); } - final tilesError = InfoPage.initInfoTiles(); - if(tilesError != null) { - errors.add(tilesError); - } - final versionError = await _initVersion(); if(versionError != null) { errors.add(versionError); @@ -103,6 +89,18 @@ Future _startApp() async { } } +class _MyHttpOverrides extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context){ + return super.createHttpClient(context) + ..badCertificateCallback = ((X509Certificate cert, String host, int port) => true); + } +} + +void _overrideHttpCertificate() { + HttpOverrides.global = _MyHttpOverrides(); // Not safe, but necessary +} + Future _initNotifications() async { try { await localNotifier.setup( @@ -174,11 +172,12 @@ void _initWindow() => doWhenWindowReady(() async { appWindow.alignment = Alignment.center; } + appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight); if(isWin11) { await Window.setEffect( effect: WindowEffect.acrylic, color: Colors.transparent, - dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark + dark: isDarkMode ); } }catch(error, stackTrace) { @@ -191,11 +190,10 @@ void _initWindow() => doWhenWindowReady(() async { Future> _initStorage() async { final errors = []; try { - await GetStorage("game", settingsDirectory.path).initStorage; - await GetStorage("backend", settingsDirectory.path).initStorage; - await GetStorage("update", settingsDirectory.path).initStorage; - await GetStorage("settings", settingsDirectory.path).initStorage; - await GetStorage("hosting", settingsDirectory.path).initStorage; + await GetStorage("game_storage", settingsDirectory.path).initStorage; + await GetStorage("backend_storage", settingsDirectory.path).initStorage; + await GetStorage("settings_storage", settingsDirectory.path).initStorage; + await GetStorage("hosting_storage", settingsDirectory.path).initStorage; }catch(error) { appWithNoStorage = true; errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage"); @@ -214,19 +212,9 @@ Future> _initStorage() async { } try { - Get.put(BuildController()); - }catch(error) { - errors.add(error); - } - - try { - Get.put(HostingController()); - }catch(error) { - errors.add(error); - } - - try { - Get.put(UpdateController()); + final controller = HostingController(); + Get.put(controller); + controller.discardServer(); }catch(error) { errors.add(error); } diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index 7937a34..ccfbecb 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -14,7 +14,6 @@ class BackendController extends GetxController { late final Rx type; late final TextEditingController gameServerAddress; late final FocusNode gameServerAddressFocusNode; - late final RxnString gameServerOwner; late final RxBool started; late final RxBool detached; StreamSubscription? worker; @@ -22,7 +21,7 @@ class BackendController extends GetxController { HttpServer? remoteServer; BackendController() { - storage = appWithNoStorage ? null : GetStorage("backend"); + storage = appWithNoStorage ? null : GetStorage("backend_storage"); started = RxBool(false); type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0)); type.listen((value) { @@ -64,8 +63,10 @@ class BackendController extends GetxController { } }); gameServerAddressFocusNode = FocusNode(); - gameServerOwner = RxnString(storage?.read("game_server_owner")); - gameServerOwner.listen((value) => storage?.write("game_server_owner", value)); + } + + void joinLocalhost() { + gameServerAddress.text = kDefaultGameServerHost; } void reset() async { @@ -147,12 +148,11 @@ class BackendController extends GetxController { switch(type()){ case ServerType.embedded: final process = await startEmbeddedBackend(detached.value); - final processPid = process.pid; - watchProcess(processPid).then((value) { - if(started()) { - started.value = false; - } - }); + watchProcess(process.pid) + .asStream() + .asBroadcastStream() + .where((_) => !started()) + .map((_) => ServerResult(ServerResultType.processError)); break; case ServerType.remote: yield ServerResult(ServerResultType.pingingRemote); diff --git a/gui/lib/src/controller/build_controller.dart b/gui/lib/src/controller/build_controller.dart deleted file mode 100644 index 8123f20..0000000 --- a/gui/lib/src/controller/build_controller.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; - -class BuildController extends GetxController { - List? _builds; - Rxn _selectedBuild; - - BuildController() : _selectedBuild = Rxn(); - - List? get builds => _builds; - - FortniteBuild? get selectedBuild => _selectedBuild.value; - - set selectedBuild(FortniteBuild? value) { - _selectedBuild.value = value; - } - - set builds(List? builds) { - _builds = builds; - _selectedBuild.value = builds?.firstOrNull; - } -} diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index be9423a..09f3585 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -25,7 +25,7 @@ class GameController extends GetxController { late final Rx consoleKey; GameController() { - _storage = appWithNoStorage ? null : GetStorage("game"); + _storage = appWithNoStorage ? null : GetStorage("game_storage"); Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]"); final decodedVersions = decodedVersionsJson .map((entry) => FortniteVersion.fromJson(entry)) @@ -33,8 +33,7 @@ class GameController extends GetxController { versions = Rx(decodedVersions); versions.listen((data) => _saveVersions()); final decodedSelectedVersionName = _storage?.read("version"); - final decodedSelectedVersion = decodedVersions.firstWhereOrNull(( - element) => element.name == decodedSelectedVersionName); + final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName); _selectedVersion = Rxn(decodedSelectedVersion); username = TextEditingController( text: _storage?.read("username") ?? kDefaultPlayerName); @@ -88,7 +87,7 @@ class GameController extends GetxController { } FortniteVersion? getVersionByName(String name) { - return versions.value.firstWhereOrNull((element) => element.name == name); + return versions.value.firstWhereOrNull((element) => element.content.toString() == name); } void addVersion(FortniteVersion version) { @@ -99,15 +98,9 @@ class GameController extends GetxController { } } - FortniteVersion removeVersionByName(String versionName) { - var version = versions.value.firstWhere((element) => element.name == versionName); - removeVersion(version); - return version; - } - void removeVersion(FortniteVersion version) { versions.update((val) => val?.remove(version)); - if (selectedVersion?.name == version.name || hasNoVersions) { + if (selectedVersion == version || hasNoVersions) { selectedVersion = null; } } @@ -125,7 +118,7 @@ class GameController extends GetxController { set selectedVersion(FortniteVersion? version) { _selectedVersion.value = version; - _storage?.write("version", version?.name); + _storage?.write("version", version?.content.toString()); } void updateVersion(FortniteVersion version, Function(FortniteVersion) function) { diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index be1d88b..64b8ac0 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -1,17 +1,25 @@ +import 'dart:convert'; + +import 'package:dart_ipify/dart_ipify.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/util/cryptography.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:sync/semaphore.dart'; import 'package:uuid/uuid.dart'; class HostingController extends GetxController { late final GetStorage? _storage; late final String uuid; late final TextEditingController name; + late final FocusNode nameFocusNode; late final TextEditingController description; + late final FocusNode descriptionFocusNode; late final TextEditingController password; + late final FocusNode passwordFocusNode; late final RxBool showPassword; late final RxBool discoverable; late final Rx type; @@ -19,10 +27,11 @@ class HostingController extends GetxController { late final RxBool started; late final RxBool published; late final Rxn instance; - late final Rxn>> servers; + late final Rxn> servers; + late final Semaphore _semaphore; HostingController() { - _storage = appWithNoStorage ? null : GetStorage("hosting"); + _storage = appWithNoStorage ? null : GetStorage("hosting_storage"); uuid = _storage?.read("uuid") ?? const Uuid().v4(); _storage?.write("uuid", uuid); name = TextEditingController(text: _storage?.read("name")); @@ -31,6 +40,9 @@ class HostingController extends GetxController { description.addListener(() => _storage?.write("description", description.text)); password = TextEditingController(text: _storage?.read("password") ?? ""); password.addListener(() => _storage?.write("password", password.text)); + nameFocusNode = FocusNode(); + descriptionFocusNode = FocusNode(); + passwordFocusNode = FocusNode(); discoverable = RxBool(_storage?.read("discoverable") ?? false); discoverable.listen((value) => _storage?.write("discoverable", value)); type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index)); @@ -43,16 +55,80 @@ class HostingController extends GetxController { instance = Rxn(); final supabase = Supabase.instance.client; servers = Rxn(); - supabase.from("hosting") + supabase.from("hosting_v2") .stream(primaryKey: ['id']) - .map((event) => _parseValidServers(event)) + .map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet()) .listen((event) { servers.value = event; - published.value = event.any((element) => element["id"] == uuid); + published.value = event.any((element) => element.id == uuid); }); + _semaphore = Semaphore(); } - Set> _parseValidServers(event) => event.where((element) => element["ip"] != null).toSet(); + Future publishServer(String author, String version) async { + try { + _semaphore.acquire(); + log("[SERVER] Publishing server..."); + if(published.value) { + log("[SERVER] Already published"); + return; + } + + final passwordText = password.text; + final hasPassword = passwordText.isNotEmpty; + var ip = await Ipify.ipv4(); + if(hasPassword) { + ip = aes256Encrypt(ip, passwordText); + } + + final supabase = Supabase.instance.client; + final hosts = supabase.from("hosting_v2"); + final payload = FortniteServer( + id: uuid, + name: name.text, + description: description.text, + author: author, + ip: ip, + version: version, + password: hasPassword ? hashPassword(passwordText) : null, + timestamp: DateTime.now(), + discoverable: discoverable.value + ).toJson(); + log("[SERVER] Payload: ${jsonEncode(payload)}"); + if(published()) { + await hosts.update(payload) + .eq("id", uuid); + }else { + await hosts.insert(payload); + } + + published.value = true; + log("[SERVER] Published"); + }catch(error) { + log("[SERVER] Cannot publish server: $error"); + published.value = false; + }finally { + _semaphore.release(); + } + } + + Future discardServer() async { + try { + _semaphore.acquire(); + log("[SERVER] Discarding server..."); + final supabase = Supabase.instance.client; + await supabase.from("hosting_v2") + .delete() + .match({'id': uuid}); + servers.value?.removeWhere((element) => element.id == uuid); + log("[SERVER] Discarded server"); + }catch(error) { + log("[SERVER] Cannot discard server: $error"); + }finally { + published.value = false; + _semaphore.release(); + } + } void reset() { name.text = ""; @@ -65,9 +141,9 @@ class HostingController extends GetxController { autoRestart.value = true; } - Map? findServerById(String uuid) { + FortniteServer? findServerById(String uuid) { try { - return servers.value?.firstWhere((element) => element["id"] == uuid); + return servers.value?.firstWhere((element) => element.id == uuid); } on StateError catch(_) { return null; } diff --git a/gui/lib/src/controller/settings_controller.dart b/gui/lib/src/controller/settings_controller.dart index 3f1a0f2..50d31ec 100644 --- a/gui/lib/src/controller/settings_controller.dart +++ b/gui/lib/src/controller/settings_controller.dart @@ -1,71 +1,333 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:version/version.dart'; +import 'package:yaml/yaml.dart'; class SettingsController extends GetxController { - late final GetStorage _storage; + late final GetStorage? _storage; late final String originalDll; late final TextEditingController gameServerDll; late final TextEditingController unrealEngineConsoleDll; late final TextEditingController backendDll; late final TextEditingController memoryLeakDll; late final TextEditingController gameServerPort; - late final RxBool firstRun; late final RxString language; late final Rx themeMode; + late final RxnInt timestamp; + late final Rx status; + late final Rx timer; + late final TextEditingController url; + late final RxBool customGameServer; + late final RxBool firstRun; + late final Map> _operations; late double width; late double height; late double? offsetX; late double? offsetY; + InfoBarEntry? infoBarEntry; + Future? _updater; SettingsController() { - _storage = GetStorage("settings"); - gameServerDll = _createController("game_server", "reboot.dll"); - unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll"); - backendDll = _createController("backend", "cobalt.dll"); - memoryLeakDll = _createController("memory_leak", "memory.dll"); - gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort); - gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text)); - width = _storage.read("width") ?? kDefaultWindowWidth; - height = _storage.read("height") ?? kDefaultWindowHeight; - offsetX = _storage.read("offset_x"); - offsetY = _storage.read("offset_y"); - firstRun = RxBool(_storage.read("first_run_new1") ?? true); - firstRun.listen((value) => _storage.write("first_run_new1", value)); - themeMode = Rx(ThemeMode.values.elementAt(_storage.read("theme") ?? 0)); - themeMode.listen((value) => _storage.write("theme", value.index)); - language = RxString(_storage.read("language") ?? currentLocale); - language.listen((value) => _storage.write("language", value)); + _storage = appWithNoStorage ? null : GetStorage("settings_storage"); + gameServerDll = _createController("game_server", InjectableDll.reboot); + unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console); + backendDll = _createController("backend", InjectableDll.cobalt); + memoryLeakDll = _createController("memory_leak", InjectableDll.memory); + gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort); + gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text)); + width = _storage?.read("width") ?? kDefaultWindowWidth; + height = _storage?.read("height") ?? kDefaultWindowHeight; + offsetX = _storage?.read("offset_x"); + offsetY = _storage?.read("offset_y"); + themeMode = Rx(ThemeMode.values.elementAt(_storage?.read("theme") ?? 0)); + themeMode.listen((value) => _storage?.write("theme", value.index)); + language = RxString(_storage?.read("language") ?? currentLocale); + language.listen((value) => _storage?.write("language", value)); + timestamp = RxnInt(_storage?.read("ts")); + timestamp.listen((value) => _storage?.write("ts", value)); + final timerIndex = _storage?.read("timer"); + timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex)); + timer.listen((value) => _storage?.write("timer", value.index)); + url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl); + url.addListener(() => _storage?.write("update_url", url.text)); + status = Rx(UpdateStatus.waiting); + customGameServer = RxBool(_storage?.read("custom_game_server") ?? false); + customGameServer.listen((value) => _storage?.write("custom_game_server", value)); + firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true); + firstRun.listen((value) => _storage?.write("first_run_tutorial", value)); + _operations = {}; } - TextEditingController _createController(String key, String name) { - var controller = TextEditingController(text: _storage.read(key) ?? _controllerDefaultPath(name)); - controller.addListener(() => _storage.write(key, controller.text)); + TextEditingController _createController(String key, InjectableDll dll) { + final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll)); + controller.addListener(() => _storage?.write(key, controller.text)); return controller; } void saveWindowSize(Size size) { - _storage.write("width", size.width); - _storage.write("height", size.height); + _storage?.write("width", size.width); + _storage?.write("height", size.height); } void saveWindowOffset(Offset position) { offsetX = position.dx; offsetY = position.dy; - _storage.write("offset_x", offsetX); - _storage.write("offset_y", offsetY); + _storage?.write("offset_x", offsetX); + _storage?.write("offset_y", offsetY); } void reset(){ - gameServerDll.text = _controllerDefaultPath("reboot.dll"); - unrealEngineConsoleDll.text = _controllerDefaultPath("console.dll"); - backendDll.text = _controllerDefaultPath("cobalt.dll"); + gameServerDll.text = _getDefaultPath(InjectableDll.reboot); + unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console); + backendDll.text = _getDefaultPath(InjectableDll.cobalt); + memoryLeakDll.text = _getDefaultPath(InjectableDll.memory); gameServerPort.text = kDefaultGameServerPort; - firstRun.value = true; + timestamp.value = null; + timer.value = UpdateTimer.never; + url.text = kRebootDownloadUrl; + status.value = UpdateStatus.waiting; + customGameServer.value = false; + updateReboot(); } - String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name"; + Future notifyLauncherUpdate() async { + if(appVersion == null) { + return; + } + + final pubspec = await _getPubspecYaml(); + if(pubspec == null) { + return; + } + + final latestVersion = Version.parse(pubspec["version"]); + if(latestVersion <= appVersion) { + return; + } + + late InfoBarEntry infoBar; + infoBar = showRebootInfoBar( + translations.updateAvailable(latestVersion.toString()), + duration: null, + severity: InfoBarSeverity.warning, + action: Button( + child: Text(translations.updateAvailableAction), + onPressed: () { + infoBar.close(); + launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases")); + }, + ) + ); + } + + Future _getPubspecYaml() async { + try { + final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml")); + if(pubspecResponse.statusCode != 200) { + return null; + } + + return loadYaml(pubspecResponse.body); + }catch(error) { + log("[UPDATER] Cannot check for updates: $error"); + return null; + } + } + + Future updateReboot({bool force = false, bool silent = false}) async { + if(_updater != null) { + return await _updater!; + } + + final result = _updateReboot(force, silent); + _updater = result; + return await result; + } + + Future _updateReboot(bool force, bool silent) async { + try { + if(customGameServer.value) { + status.value = UpdateStatus.success; + return true; + } + + final needsUpdate = await hasRebootDllUpdate( + timestamp.value, + hours: timer.value.hours, + force: force + ); + if(!needsUpdate) { + status.value = UpdateStatus.success; + return true; + } + + if(!silent) { + infoBarEntry = showRebootInfoBar( + translations.downloadingDll("reboot"), + loading: true, + duration: null + ); + } + timestamp.value = await downloadRebootDll(url.text); + status.value = UpdateStatus.success; + infoBarEntry?.close(); + if(!silent) { + infoBarEntry = showRebootInfoBar( + translations.downloadDllSuccess("reboot"), + severity: InfoBarSeverity.success, + duration: infoBarShortDuration + ); + } + return true; + }catch(message) { + infoBarEntry?.close(); + var error = message.toString(); + error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; + error = error.toLowerCase(); + status.value = UpdateStatus.error; + showRebootInfoBar( + translations.downloadDllError("reboot.dll", error.toString()), + duration: infoBarLongDuration, + severity: InfoBarSeverity.error, + action: Button( + onPressed: () => updateReboot( + force: true, + silent: silent + ), + child: Text(translations.downloadDllRetry), + ) + ); + return false; + }finally { + _updater = null; + } + } + + (File, bool) getInjectableData(InjectableDll dll) { + final defaultPath = canonicalize(_getDefaultPath(dll)); + switch(dll){ + case InjectableDll.reboot: + if(customGameServer.value) { + final file = File(gameServerDll.text); + if(file.existsSync()) { + return (file, true); + } + } + + return (rebootDllFile, false); + case InjectableDll.console: + final ue4ConsoleFile = File(unrealEngineConsoleDll.text); + return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath); + case InjectableDll.cobalt: + final backendFile = File(backendDll.text); + return (backendFile, canonicalize(backendFile.path) != defaultPath); + case InjectableDll.memory: + final memoryLeakFile = File(memoryLeakDll.text); + return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath); + } + } + + String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll"; + + Future downloadCriticalDllInteractive(String filePath, {bool silent = false}) { + log("[DLL] Asking for $filePath(silent: $silent)"); + final old = _operations[filePath]; + if(old != null) { + log("[DLL] Download task already exists"); + return old; + } + + log("[DLL] Creating new download task..."); + final newRun = _downloadCriticalDllInteractive(filePath, silent); + _operations[filePath] = newRun; + return newRun; + } + + Future _downloadCriticalDllInteractive(String filePath, bool silent) async { + final fileName = basename(filePath).toLowerCase(); + log("[DLL] File name: $fileName"); + InfoBarEntry? entry; + try { + if (fileName == "reboot.dll") { + log("[DLL] Downloading reboot.dll..."); + return await updateReboot( + silent: silent + ); + } + + if(File(filePath).existsSync()) { + log("[DLL] File already exists"); + return true; + } + + final fileNameWithoutExtension = basenameWithoutExtension(filePath); + if(!silent) { + entry = showRebootInfoBar( + translations.downloadingDll(fileNameWithoutExtension), + loading: true, + duration: null + ); + } + await downloadCriticalDll(fileName, filePath); + entry?.close(); + if(!silent) { + entry = await showRebootInfoBar( + translations.downloadDllSuccess(fileNameWithoutExtension), + severity: InfoBarSeverity.success, + duration: infoBarShortDuration + ); + } + return true; + }catch(message) { + log("[DLL] Error: $message"); + entry?.close(); + var error = message.toString(); + error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; + error = error.toLowerCase(); + final completer = Completer(); + await showRebootInfoBar( + translations.downloadDllError(fileName, error.toString()), + duration: infoBarLongDuration, + severity: InfoBarSeverity.error, + onDismissed: () => completer.complete(null), + action: Button( + onPressed: () async { + await downloadCriticalDllInteractive(filePath); + completer.complete(null); + }, + child: Text(translations.downloadDllRetry), + ) + ); + await completer.future; + return false; + }finally { + _operations.remove(fileName); + } + } } + +extension _UpdateTimerExtension on UpdateTimer { + int get hours { + switch(this) { + case UpdateTimer.never: + return -1; + case UpdateTimer.hour: + return 1; + case UpdateTimer.day: + return 24; + case UpdateTimer.week: + return 24 * 7; + } + } +} \ No newline at end of file diff --git a/gui/lib/src/controller/update_controller.dart b/gui/lib/src/controller/update_controller.dart deleted file mode 100644 index 781c79a..0000000 --- a/gui/lib/src/controller/update_controller.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:http/http.dart' as http; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/main.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/util/translations.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:version/version.dart'; -import 'package:yaml/yaml.dart'; - -class UpdateController { - late final GetStorage? _storage; - late final RxnInt timestamp; - late final Rx status; - late final Rx timer; - late final TextEditingController url; - late final RxBool customGameServer; - InfoBarEntry? infoBarEntry; - Future? _updater; - - UpdateController() { - _storage = appWithNoStorage ? null : GetStorage("update"); - timestamp = RxnInt(_storage?.read("ts")); - timestamp.listen((value) => _storage?.write("ts", value)); - var timerIndex = _storage?.read("timer"); - timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex)); - timer.listen((value) => _storage?.write("timer", value.index)); - url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl); - url.addListener(() => _storage?.write("update_url", url.text)); - status = Rx(UpdateStatus.waiting); - customGameServer = RxBool(_storage?.read("custom_game_server") ?? false); - customGameServer.listen((value) => _storage?.write("custom_game_server", value)); - } - - Future notifyLauncherUpdate() async { - if(appVersion == null) { - return; - } - - final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml")); - if(pubspecResponse.statusCode != 200) { - return; - } - - final pubspec = loadYaml(pubspecResponse.body); - final latestVersion = Version.parse(pubspec["version"]); - if(latestVersion <= appVersion) { - return; - } - - late InfoBarEntry infoBar; - infoBar = showInfoBar( - translations.updateAvailable(latestVersion.toString()), - duration: null, - severity: InfoBarSeverity.warning, - action: Button( - child: Text(translations.updateAvailableAction), - onPressed: () { - infoBar.close(); - launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases")); - }, - ) - ); - } - - Future updateReboot({bool force = false, bool silent = false}) async { - if(_updater != null) { - return await _updater; - } - - final result = _updateReboot(force, silent); - _updater = result; - return await result; - } - - Future _updateReboot(bool force, bool silent) async { - try { - if(customGameServer.value) { - status.value = UpdateStatus.success; - return; - } - - final needsUpdate = await hasRebootDllUpdate( - timestamp.value, - hours: timer.value.hours, - force: force - ); - if(!needsUpdate) { - status.value = UpdateStatus.success; - return; - } - - if(!silent) { - infoBarEntry = showInfoBar( - translations.downloadingDll("reboot"), - loading: true, - duration: null - ); - } - timestamp.value = await downloadRebootDll(url.text); - status.value = UpdateStatus.success; - infoBarEntry?.close(); - if(!silent) { - infoBarEntry = showInfoBar( - translations.downloadDllSuccess("reboot"), - severity: InfoBarSeverity.success, - duration: infoBarShortDuration - ); - } - }catch(message) { - if(!silent) { - infoBarEntry?.close(); - var error = message.toString(); - error = - error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; - error = error.toLowerCase(); - status.value = UpdateStatus.error; - showInfoBar( - translations.downloadDllError("reboot.dll", error.toString()), - duration: infoBarLongDuration, - severity: InfoBarSeverity.error, - action: Button( - onPressed: () => updateReboot( - force: true, - silent: silent - ), - child: Text(translations.downloadDllRetry), - ) - ); - } - }finally { - _updater = null; - } - } - - void reset() { - timestamp.value = null; - timer.value = UpdateTimer.never; - url.text = kRebootDownloadUrl; - status.value = UpdateStatus.waiting; - customGameServer.value = false; - updateReboot(); - } -} - -extension _UpdateTimerExtension on UpdateTimer { - int get hours { - switch(this) { - case UpdateTimer.never: - return -1; - case UpdateTimer.hour: - return 1; - case UpdateTimer.day: - return 24; - case UpdateTimer.week: - return 24 * 7; - } - } -} \ No newline at end of file diff --git a/gui/lib/src/dialog/abstract/dialog_button.dart b/gui/lib/src/dialog/abstract/dialog_button.dart deleted file mode 100644 index 0c42227..0000000 --- a/gui/lib/src/dialog/abstract/dialog_button.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/translations.dart'; - -class DialogButton extends StatefulWidget { - final String? text; - final Function()? onTap; - final ButtonType type; - - const DialogButton( - {Key? key, - this.text, - this.onTap, - required this.type}) - : assert(type != ButtonType.primary || onTap != null, - "OnTap handler cannot be null for primary buttons"), - assert(type != ButtonType.primary || text != null, - "Text cannot be null for primary buttons"), - super(key: key); - - @override - State createState() => _DialogButtonState(); -} - -class _DialogButtonState extends State { - @override - Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button; - - SizedBox get _onlyButton => SizedBox( - width: double.infinity, - child: _button - ); - - Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton; - - Widget get _primaryButton { - return Button( - onPressed: widget.onTap!, - child: Text(widget.text!), - ); - } - - Widget get _secondaryButton { - return Button( - onPressed: widget.onTap ?? _onDefaultSecondaryActionTap, - child: Text(widget.text ?? translations.defaultDialogSecondaryAction), - ); - } - - void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null); -} - -enum ButtonType { - primary, - secondary, - only -} diff --git a/gui/lib/src/dialog/abstract/dialog.dart b/gui/lib/src/messenger/abstract/dialog.dart similarity index 74% rename from gui/lib/src/dialog/abstract/dialog.dart rename to gui/lib/src/messenger/abstract/dialog.dart index cdca555..1c96404 100644 --- a/gui/lib/src/dialog/abstract/dialog.dart +++ b/gui/lib/src/messenger/abstract/dialog.dart @@ -1,21 +1,20 @@ import 'package:clipboard/clipboard.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent show showDialog; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -import 'dialog_button.dart'; - bool inDialog = false; -Future showAppDialog({required WidgetBuilder builder}) async { +Future showRebootDialog({required WidgetBuilder builder, bool dismissWithEsc = true}) async { inDialog = true; pagesController.add(null); try { return await fluent.showDialog( - context: appKey.currentContext!, + context: appNavigatorKey.currentContext!, useRootNavigator: false, + dismissWithEsc: dismissWithEsc, builder: builder ); }finally { @@ -58,7 +57,7 @@ class FormDialog extends AbstractDialog { return Form( child: Builder( builder: (context) { - var parsed = buttons.map((entry) => _createFormButton(entry, context)).toList(); + final parsed = buttons.map((entry) => _createFormButton(entry, context)).toList(); return GenericDialog( header: content, buttons: parsed @@ -117,8 +116,9 @@ class InfoDialog extends AbstractDialog { class ProgressDialog extends AbstractDialog { final String text; final Function()? onStop; + final bool showButton; - const ProgressDialog({required this.text, this.onStop, Key? key}) : super(key: key); + const ProgressDialog({required this.text, this.onStop, this.showButton = true, Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -132,11 +132,12 @@ class ProgressDialog extends AbstractDialog { ), ), buttons: [ - DialogButton( - text: translations.defaultDialogSecondaryAction, - type: ButtonType.only, - onTap: onStop - ) + if(showButton) + DialogButton( + text: translations.defaultDialogSecondaryAction, + type: ButtonType.only, + onTap: onStop + ) ] ); } @@ -239,7 +240,7 @@ class ErrorDialog extends AbstractDialog { type: type, onTap: () async { FlutterClipboard.controlC("$error\n$stackTrace"); - showInfoBar(translations.copyErrorDialogSuccess); + showRebootInfoBar(translations.copyErrorDialogSuccess); onClick(); }, ); @@ -262,4 +263,62 @@ class ErrorDialog extends AbstractDialog { ], ); } +} + +class DialogButton extends StatefulWidget { + final String? text; + final Function()? onTap; + final ButtonType type; + final Color? color; + + const DialogButton( + {Key? key, + this.text, + this.onTap, + this.color, + required this.type}) + : assert(type != ButtonType.primary || onTap != null, + "OnTap handler cannot be null for primary buttons"), + assert(type != ButtonType.primary || text != null, + "Text cannot be null for primary buttons"), + super(key: key); + + @override + State createState() => _DialogButtonState(); +} + +class _DialogButtonState extends State { + @override + Widget build(BuildContext context) => widget.type == ButtonType.only ? _onlyButton : _button; + + SizedBox get _onlyButton => SizedBox( + width: double.infinity, + child: _button + ); + + Widget get _button => widget.type == ButtonType.primary ? _primaryButton : _secondaryButton; + + Widget get _primaryButton => Button( + style: ButtonStyle( + backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor) + ), + onPressed: widget.onTap!, + child: Text(widget.text!), + ); + + Widget get _secondaryButton => Button( + style: widget.color != null ? ButtonStyle( + backgroundColor: ButtonState.all(widget.color!) + ) : null, + onPressed: widget.onTap ?? _onDefaultSecondaryActionTap, + child: Text(widget.text ?? translations.defaultDialogSecondaryAction), + ); + + void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null); +} + +enum ButtonType { + primary, + secondary, + only } \ No newline at end of file diff --git a/gui/lib/src/dialog/abstract/info_bar.dart b/gui/lib/src/messenger/abstract/info_bar.dart similarity index 56% rename from gui/lib/src/dialog/abstract/info_bar.dart rename to gui/lib/src/messenger/abstract/info_bar.dart index c6a7e87..0887531 100644 --- a/gui/lib/src/dialog/abstract/info_bar.dart +++ b/gui/lib/src/messenger/abstract/info_bar.dart @@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4); const infoBarShortDuration = Duration(seconds: 2); const _height = 64.0; -InfoBarEntry showInfoBar(dynamic text, { +InfoBarEntry showRebootInfoBar(dynamic text, { InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = infoBarShortDuration, @@ -21,33 +21,40 @@ InfoBarEntry showInfoBar(dynamic text, { return overlayEntry; } -Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => SizedBox( - width: double.infinity, - height: _height, +Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox( + constraints: BoxConstraints( + minWidth: double.infinity, + minHeight: _height + ), child: Mica( - child: InfoBar( - 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: SizedBox( - width: double.infinity, - child: loading ? const Padding( - padding: EdgeInsets.only(top: 8.0, bottom: 2.0), - child: ProgressBar(), - ) : const SizedBox() - ), - severity: severity - ), + elevation: 1, + child: InfoBar( + 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: SizedBox( + width: double.infinity, + child: loading ? const Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 2.0, + right: 6.0 + ), + child: ProgressBar(), + ) : const SizedBox() + ), + severity: severity + ) ), ); diff --git a/gui/lib/src/messenger/abstract/overlay.dart b/gui/lib/src/messenger/abstract/overlay.dart new file mode 100644 index 0000000..f2038b8 --- /dev/null +++ b/gui/lib/src/messenger/abstract/overlay.dart @@ -0,0 +1,170 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/rendering.dart'; +import 'package:reboot_launcher/src/page/implementation/home_page.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; + +typedef WidgetBuilder = Widget Function(BuildContext, void Function()); + +class OverlayTarget extends StatefulWidget { + final Widget child; + const OverlayTarget({super.key, required this.child}); + + @override + State createState() => OverlayTargetState(); + + OverlayTargetState of(BuildContext context) => context.findAncestorStateOfType()!; +} + +class OverlayTargetState extends State { + @override + Widget build(BuildContext context) => widget.child; + + void showOverlay({ + required String text, + required WidgetBuilder actionBuilder, + Offset offset = Offset.zero, + bool ignoreTargetPointers = true, + AttachMode attachMode = AttachMode.start + }) { + final renderBox = context.findRenderObject() as RenderBox; + final position = renderBox.localToGlobal(Offset.zero); + final color = FluentTheme.of(context).acrylicBackgroundColor; + late OverlayEntry entry; + entry = OverlayEntry( + builder: (context) => Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + child: _AbsorbPointer( + exclusion: ignoreTargetPointers ? null : renderBox + ) + ), + Positioned( + left: position.dx - (attachMode != AttachMode.start ? renderBox.size.width : 0) + offset.dx, + top: position.dy + (renderBox.size.height / 2) + offset.dy, + child: CustomPaint( + painter: _CallOutShape(color, attachMode != AttachMode.start), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(text), + const SizedBox(height: 12.0), + actionBuilder(context, () => entry.remove()) + ], + ), + ), + ), + ), + ], + ) + ); + appOverlayKey.currentState?.insert(entry); + } +} + +enum AttachMode { + start, + middle, + end; +} + +// Harder than one would think +class _CallOutShape extends CustomPainter { + final Color color; + final bool end; + _CallOutShape(this.color, this.end); + + @override + void paint(Canvas canvas, Size size) { + final fillPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 0.25 + ..color = Colors.white; + + final path = Path(); + path.moveTo(10, 0); + if(!end) { + path.lineTo(12.5, 0); + path.lineTo(20, -12.5); + path.lineTo(27.5, 0); + }else { + path.lineTo(size.width - 27.5, 0); + path.lineTo(size.width - 20, -12.5); + path.lineTo(size.width - 12.5, 0); + } + + path.lineTo(size.width - 10, 0); + path.arcToPoint(Offset(size.width, 10), radius: Radius.circular(10)); + path.lineTo(size.width, size.height - 10); + path.arcToPoint(Offset(size.width - 10, size.height), radius: Radius.circular(10)); + path.lineTo(10, size.height); + path.arcToPoint(Offset(0, size.height - 10), radius: Radius.circular(10)); + path.lineTo(0, 10); + path.arcToPoint(Offset(10, 0), radius: Radius.circular(10)); + path.close(); + + canvas.drawPath(path, fillPaint); + canvas.drawPath(path, borderPaint); + canvas.drawShadow(path, color, 1, true); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +class _AbsorbPointer extends SingleChildRenderObjectWidget { + final RenderBox? exclusion; + const _AbsorbPointer({ + required this.exclusion + }); + + @override + _RenderAbsorbPointer createRenderObject(BuildContext context) => _RenderAbsorbPointer( + exclusion: exclusion + ); +} + +class _RenderAbsorbPointer extends RenderProxyBox { + final RenderBox? exclusion; + _RenderAbsorbPointer({ + required this.exclusion, + RenderBox? child + }) : super(child); + + @override + bool hitTest(BoxHitTestResult result, { required Offset position }) { + final exclusion = this.exclusion; + if(exclusion == null) { + return size.contains(position); + } + + // 32 is the height of the title bar (need this offset as the overlay area doesn't include it) + // Not an optimal solution but it works (calculating it is kind of complicated) + position = Offset(position.dx, position.dy + HomePage.kTitleBarHeight); + final exclusionPosition = exclusion.localToGlobal(Offset.zero); + final exclusionSize = Rect.fromLTRB( + exclusionPosition.dx, + exclusionPosition.dy, + exclusionPosition.dx + exclusion.size.width, + exclusionPosition.dy + exclusion.size.height + ); + return !exclusionSize.contains(position); + } + + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) => super.visitChildrenForSemantics(visitor); + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.isBlockingUserActions = true; + } +} diff --git a/gui/lib/src/dialog/implementation/data.dart b/gui/lib/src/messenger/implementation/data.dart similarity index 74% rename from gui/lib/src/dialog/implementation/data.dart rename to gui/lib/src/messenger/implementation/data.dart index e4767bd..d4ffab3 100644 --- a/gui/lib/src/dialog/implementation/data.dart +++ b/gui/lib/src/messenger/implementation/data.dart @@ -1,9 +1,8 @@ import 'package:fluent_ui/fluent_ui.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/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -Future showResetDialog(Function() onConfirm) => showAppDialog( +Future showResetDialog(Function() onConfirm) => showRebootDialog( builder: (context) => InfoDialog( text: translations.resetDefaultsDialogTitle, buttons: [ diff --git a/gui/lib/src/dialog/implementation/dll.dart b/gui/lib/src/messenger/implementation/dll.dart similarity index 72% rename from gui/lib/src/dialog/implementation/dll.dart rename to gui/lib/src/messenger/implementation/dll.dart index 5c1e7c2..00b2bd8 100644 --- a/gui/lib/src/dialog/implementation/dll.dart +++ b/gui/lib/src/messenger/implementation/dll.dart @@ -1,9 +1,8 @@ import 'package:fluent_ui/fluent_ui.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/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -Future showDllDeletedDialog(Function() onConfirm) => showAppDialog( +Future showDllDeletedDialog(Function() onConfirm) => showRebootDialog( builder: (context) => InfoDialog( text: translations.dllDeletedTitle, buttons: [ diff --git a/gui/lib/src/dialog/implementation/error.dart b/gui/lib/src/messenger/implementation/error.dart similarity index 81% rename from gui/lib/src/dialog/implementation/error.dart rename to gui/lib/src/messenger/implementation/error.dart index b9eb94f..802aa6b 100644 --- a/gui/lib/src/dialog/implementation/error.dart +++ b/gui/lib/src/messenger/implementation/error.dart @@ -1,12 +1,9 @@ import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -import '../../util/log.dart'; - - String? lastError; void onError(Object exception, StackTrace? stackTrace, bool framework) { @@ -21,12 +18,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) { } lastError = exception.toString(); - var route = ModalRoute.of(pageKey.currentContext!); + final route = ModalRoute.of(pageKey.currentContext!); if(route != null && !route.isCurrent){ Navigator.of(pageKey.currentContext!).pop(false); } - WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showAppDialog( + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog( builder: (context) => ErrorDialog( exception: exception, diff --git a/gui/lib/src/messenger/implementation/onboard.dart b/gui/lib/src/messenger/implementation/onboard.dart new file mode 100644 index 0000000..57c9962 --- /dev/null +++ b/gui/lib/src/messenger/implementation/onboard.dart @@ -0,0 +1,346 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/backend_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; +import 'package:reboot_launcher/src/messenger/implementation/profile.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/page/implementation/backend_page.dart'; +import 'package:reboot_launcher/src/page/implementation/home_page.dart'; +import 'package:reboot_launcher/src/page/implementation/host_page.dart'; +import 'package:reboot_launcher/src/page/implementation/play_page.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/version_selector.dart'; + +void startOnboarding() { + final settingsController = Get.find(); + settingsController.firstRun.value = false; + profileOverlayKey.currentState!.showOverlay( + text: translations.startOnboardingText, + offset: Offset(27.5, 17.5), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.startOnboardingActionLabel, + onTap: () async { + onClose(); + await showProfileForm(context); + _promptPlayPage(); + } + ) + ); +} + +void _promptPlayPage() { + pageIndex.value = RebootPageType.play.index; + pageOverlayTargetKey.currentState!.showOverlay( + text: translations.promptPlayPageText, + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptPlayPageActionLabel, + onTap: () async { + onClose(); + _promptPlayVersion(); + } + ) + ); +} + +void _promptPlayVersion() { + final gameController = Get.find(); + final hasBuilds = gameController.versions.value.isNotEmpty; + gameVersionOverlayTargetKey.currentState!.showOverlay( + text: translations.promptPlayVersionText, + attachMode: AttachMode.middle, + offset: Offset(-25, 0), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: hasBuilds ? translations.promptPlayVersionActionLabelHasBuilds : translations.promptPlayVersionActionLabelNoBuilds, + onTap: () async { + onClose(); + if(!hasBuilds) { + await VersionSelector.openDownloadDialog(closable: false); + } + _promptServerBrowserPage(); + } + ) + ); +} + +void _promptServerBrowserPage() { + pageIndex.value = RebootPageType.browser.index; + pageOverlayTargetKey.currentState!.showOverlay( + text: translations.promptServerBrowserPageText, + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptServerBrowserPageActionLabel, + onTap: () { + onClose(); + _promptHostPage(); + } + ) + ); +} + +void _promptHostPage() { + pageIndex.value = RebootPageType.host.index; + pageOverlayTargetKey.currentState!.showOverlay( + text: translations.promptHostPageText, + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptHostPageActionLabel, + onTap: () { + onClose(); + _promptHostInfo(); + } + ) + ); +} + + +void _promptHostInfo() { + final hostingController = Get.find(); + hostInfoOverlayTargetKey.currentState!.showOverlay( + text: translations.promptHostInfoText, + offset: Offset(-10, 2.5), + actionBuilder: (context, onClose) => Row( + children: [ + _buildActionButton( + context: context, + label: translations.promptHostInfoActionLabelSkip, + themed: false, + onTap: () { + onClose(); + hostingController.discoverable.value = false; + _promptHostVersion(); + } + ), + const SizedBox(width: 12.0), + _buildActionButton( + context: context, + label: translations.promptHostInfoActionLabelConfigure, + onTap: () { + onClose(); + hostingController.discoverable.value = true; + hostInfoTileKey.currentState!.openNestedPage(); + WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostInformation()); + } + ) + ], + ) + ); +} + +void _promptHostInformation() { + final hostingController = Get.find(); + hostingController.nameFocusNode.requestFocus(); + hostInfoNameOverlayTargetKey.currentState!.showOverlay( + text: translations.promptHostInformationText, + attachMode: AttachMode.middle, + ignoreTargetPointers: false, + offset: Offset(100, 0), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptHostInformationActionLabel, + onTap: () { + onClose(); + _promptHostInformationDescription(); + } + ) + ); +} + +void _promptHostInformationDescription() { + final hostingController = Get.find(); + hostingController.descriptionFocusNode.requestFocus(); + hostInfoDescriptionOverlayTargetKey.currentState!.showOverlay( + text: translations.promptHostInformationDescriptionText, + attachMode: AttachMode.middle, + ignoreTargetPointers: false, + offset: Offset(70, 0), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptHostInformationDescriptionActionLabel, + onTap: () { + onClose(); + _promptHostInformationPassword(); + } + ) + ); +} + +void _promptHostInformationPassword() { + final hostingController = Get.find(); + hostingController.passwordFocusNode.requestFocus(); + hostInfoPasswordOverlayTargetKey.currentState!.showOverlay( + text: translations.promptHostInformationPasswordText, + ignoreTargetPointers: false, + attachMode: AttachMode.middle, + offset: Offset(25, 0), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptHostInformationPasswordActionLabel, + onTap: () { + onClose(); + Navigator.of(hostInfoTileKey.currentContext!).pop(); + pageStack.removeLast(); + WidgetsBinding.instance.addPostFrameCallback((_) => _promptHostVersion()); + } + ) + ); +} + +void _promptHostVersion() { + hostVersionOverlayTargetKey.currentState!.showOverlay( + text: translations.promptHostVersionText, + attachMode: AttachMode.end, + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptHostVersionActionLabel, + onTap: () { + onClose(); + _promptHostShare(); + } + ) + ); +} + +void _promptHostShare() { + final backendController = Get.find(); + hostShareOverlayTargetKey.currentState!.showOverlay( + text: translations.promptHostShareText, + offset: Offset(-10, 2.5), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptHostShareActionLabel, + onTap: () { + onClose(); + backendController.type.value = ServerType.embedded; + _promptBackendPage(); + } + ) + ); +} + + +void _promptBackendPage() { + pageIndex.value = RebootPageType.backend.index; + pageOverlayTargetKey.currentState!.showOverlay( + text: translations.promptBackendPageText, + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptBackendPageActionLabel, + onTap: () { + onClose(); + _promptBackendTypePage(); + } + ) + ); +} + +void _promptBackendTypePage() { + backendTypeOverlayTargetKey.currentState!.showOverlay( + text: translations.promptBackendTypePageText, + attachMode: AttachMode.end, + offset: Offset(-25, 0), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptBackendTypePageActionLabel, + onTap: () { + onClose(); + _promptBackendGameServerAddress(); + } + ) + ); +} + +void _promptBackendGameServerAddress() { + backendGameServerAddressOverlayTargetKey.currentState!.showOverlay( + text: translations.promptBackendGameServerAddressText, + attachMode: AttachMode.end, + offset: Offset(-100, 0), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptBackendGameServerAddressActionLabel, + onTap: () { + onClose(); + _promptBackendUnrealEngineKey(); + } + ) + ); +} + +void _promptBackendUnrealEngineKey() { + backendUnrealEngineOverlayTargetKey.currentState!.showOverlay( + text: translations.promptBackendUnrealEngineKeyText, + attachMode: AttachMode.end, + offset: Offset(-465, 2.5), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptBackendUnrealEngineKeyActionLabel, + onTap: () { + onClose(); + _promptBackendDetached(); + } + ) + ); +} + +void _promptBackendDetached() { + backendDetachedOverlayTargetKey.currentState!.showOverlay( + text: translations.promptBackendDetachedText, + attachMode: AttachMode.end, + offset: Offset(-410, 2.5), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptBackendDetachedActionLabel, + onTap: () { + onClose(); + _promptInfoTab(); + } + ) + ); +} + +void _promptInfoTab() { + pageIndex.value = RebootPageType.info.index; + pageOverlayTargetKey.currentState!.showOverlay( + text: translations.promptInfoTabText, + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptInfoTabActionLabel, + onTap: () { + onClose(); + _promptSettingsTab(); + } + ) + ); +} + +void _promptSettingsTab() { + pageIndex.value = RebootPageType.settings.index; + pageOverlayTargetKey.currentState!.showOverlay( + text: translations.promptSettingsTabText, + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.promptSettingsTabActionLabel, + onTap: onClose + ) + ); +} + +Widget _buildActionButton({ + required BuildContext context, + required String label, + bool themed = true, + required void Function() onTap, +}) => Button( + style: themed ? ButtonStyle( + backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor) + ) : null, + child: Text(label), + onPressed: onTap +); diff --git a/gui/lib/src/dialog/implementation/profile.dart b/gui/lib/src/messenger/implementation/profile.dart similarity index 88% rename from gui/lib/src/dialog/implementation/profile.dart rename to gui/lib/src/messenger/implementation/profile.dart index ebd81b8..fc09bd2 100644 --- a/gui/lib/src/dialog/implementation/profile.dart +++ b/gui/lib/src/messenger/implementation/profile.dart @@ -2,18 +2,17 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart' show Icons; import 'package:get/get.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/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/util/translations.dart'; final GameController _gameController = Get.find(); Future showProfileForm(BuildContext context) async{ - var showPassword = RxBool(false); - var oldUsername = _gameController.username.text; - var showPasswordTrailing = RxBool(oldUsername.isNotEmpty); - var oldPassword = _gameController.password.text; - var result = await showAppDialog( + final showPassword = RxBool(false); + final oldUsername = _gameController.username.text; + final showPasswordTrailing = RxBool(oldUsername.isNotEmpty); + final oldPassword = _gameController.password.text; + final result = await showRebootDialog( builder: (context) => Obx(() => FormDialog( content: Column( mainAxisSize: MainAxisSize.min, diff --git a/gui/lib/src/dialog/implementation/server.dart b/gui/lib/src/messenger/implementation/server.dart similarity index 69% rename from gui/lib/src/dialog/implementation/server.dart rename to gui/lib/src/messenger/implementation/server.dart index f7df30f..a878505 100644 --- a/gui/lib/src/dialog/implementation/server.dart +++ b/gui/lib/src/messenger/implementation/server.dart @@ -1,24 +1,20 @@ import 'dart:async'; import 'package:clipboard/clipboard.dart'; -import 'package:dart_ipify/dart_ipify.dart'; import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; -import 'package:reboot_launcher/src/controller/hosting_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/controller/game_controller.dart'; +import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/cryptography.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:sync/semaphore.dart'; - -final Semaphore _publishingSemaphore = Semaphore(); extension ServerControllerDialog on BackendController { Future toggleInteractive() async { @@ -39,108 +35,108 @@ extension ServerControllerDialog on BackendController { } InfoBarEntry _handeEvent(ServerResult event) { - switch (event.type) { + log("[BACKEND] Handling event: $event"); + switch (event.type) { case ServerResultType.starting: - return showInfoBar( + return showRebootInfoBar( translations.startingServer, severity: InfoBarSeverity.info, loading: true, duration: null ); case ServerResultType.startSuccess: - return showInfoBar( + return showRebootInfoBar( type.value == ServerType.local ? translations.checkedServer : translations.startedServer, severity: InfoBarSeverity.success ); case ServerResultType.startError: print(event.stackTrace); - return showInfoBar( + return showRebootInfoBar( type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError), severity: InfoBarSeverity.error, duration: infoBarLongDuration ); case ServerResultType.stopping: - return showInfoBar( + return showRebootInfoBar( translations.stoppingServer, severity: InfoBarSeverity.info, loading: true, duration: null ); case ServerResultType.stopSuccess: - return showInfoBar( + return showRebootInfoBar( translations.stoppedServer, severity: InfoBarSeverity.success ); case ServerResultType.stopError: - return showInfoBar( + return showRebootInfoBar( translations.stopServerError(event.error ?? translations.unknownError), severity: InfoBarSeverity.error, duration: infoBarLongDuration ); case ServerResultType.missingHostError: - return showInfoBar( + return showRebootInfoBar( translations.missingHostNameError, severity: InfoBarSeverity.error ); case ServerResultType.missingPortError: - return showInfoBar( + return showRebootInfoBar( translations.missingPortError, severity: InfoBarSeverity.error ); case ServerResultType.illegalPortError: - return showInfoBar( + return showRebootInfoBar( translations.illegalPortError, severity: InfoBarSeverity.error ); case ServerResultType.freeingPort: - return showInfoBar( + return showRebootInfoBar( translations.freeingPort, loading: true, duration: null ); case ServerResultType.freePortSuccess: - return showInfoBar( + return showRebootInfoBar( translations.freedPort, severity: InfoBarSeverity.success, duration: infoBarShortDuration ); case ServerResultType.freePortError: - return showInfoBar( + return showRebootInfoBar( translations.freePortError(event.error ?? translations.unknownError), severity: InfoBarSeverity.error, duration: infoBarLongDuration ); case ServerResultType.pingingRemote: - return showInfoBar( - translations.pingingRemoteServer, + return showRebootInfoBar( + translations.pingingServer(ServerType.remote.name), severity: InfoBarSeverity.info, loading: true, duration: null ); case ServerResultType.pingingLocal: - return showInfoBar( - translations.pingingLocalServer(type.value.name), + return showRebootInfoBar( + translations.pingingServer(type.value.name), severity: InfoBarSeverity.info, loading: true, duration: null ); case ServerResultType.pingError: - return showInfoBar( + return showRebootInfoBar( translations.pingError(type.value.name), severity: InfoBarSeverity.error ); + case ServerResultType.processError: + return showRebootInfoBar( + translations.backendProcessError, + severity: InfoBarSeverity.error + ); } } - void joinLocalHost() { - gameServerAddress.text = kDefaultGameServerHost; - gameServerOwner.value = null; - } - - Future joinServer(String uuid, Map entry) async { - final id = entry["id"]; - if(uuid == id) { - showInfoBar( + Future joinServerInteractive(String uuid, FortniteServer server) async { + if(!kDebugMode && uuid == server.id) { + showRebootInfoBar( translations.joinSelfServer, duration: infoBarLongDuration, severity: InfoBarSeverity.error @@ -148,18 +144,29 @@ extension ServerControllerDialog on BackendController { return; } - final hashedPassword = entry["password"]; + final gameController = Get.find(); + final version = gameController.getVersionByName(server.version.toString()); + if(version == null) { + showRebootInfoBar( + translations.cannotJoinServerVersion(server.version.toString()), + duration: infoBarLongDuration, + severity: InfoBarSeverity.error + ); + return; + } + + final hashedPassword = server.password; final hasPassword = hashedPassword != null; final embedded = type.value == ServerType.embedded; - final author = entry["author"]; - final encryptedIp = entry["ip"]; + final author = server.author; + final encryptedIp = server.ip; if(!hasPassword) { final valid = await _isServerValid(encryptedIp); if(!valid) { return; } - _onSuccess(embedded, encryptedIp, author); + _onSuccess(gameController, embedded, encryptedIp, author, version); return; } @@ -169,7 +176,7 @@ extension ServerControllerDialog on BackendController { } if(!checkPassword(confirmPassword, hashedPassword)) { - showInfoBar( + showRebootInfoBar( translations.wrongServerPassword, duration: infoBarLongDuration, severity: InfoBarSeverity.error @@ -183,7 +190,7 @@ extension ServerControllerDialog on BackendController { return; } - _onSuccess(embedded, decryptedIp, author); + _onSuccess(gameController, embedded, decryptedIp, author, version); } Future _isServerValid(String address) async { @@ -192,7 +199,7 @@ extension ServerControllerDialog on BackendController { return true; } - showInfoBar( + showRebootInfoBar( translations.offlineServer, duration: infoBarLongDuration, severity: InfoBarSeverity.error @@ -204,7 +211,7 @@ extension ServerControllerDialog on BackendController { final confirmPasswordController = TextEditingController(); final showPassword = RxBool(false); final showPasswordTrailing = RxBool(false); - return await showAppDialog( + return await showRebootDialog( builder: (context) => FormDialog( content: Column( mainAxisSize: MainAxisSize.min, @@ -253,80 +260,18 @@ extension ServerControllerDialog on BackendController { ); } - void _onSuccess(bool embedded, String decryptedIp, String author) { + void _onSuccess(GameController controller, bool embedded, String decryptedIp, String author, FortniteVersion version) { if(embedded) { gameServerAddress.text = decryptedIp; - gameServerOwner.value = author; - pageIndex.value = 0; + pageIndex.value = RebootPageType.play.index; }else { FlutterClipboard.controlC(decryptedIp); } - WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar( + controller.selectedVersion = version; + WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar( embedded ? translations.joinedServer(author) : translations.copiedIp, duration: infoBarLongDuration, severity: InfoBarSeverity.success )); } -} - -extension HostingControllerExtension on HostingController { - Future publishServer(String author, String version) async { - try { - _publishingSemaphore.acquire(); - if(published.value) { - return; - } - - final passwordText = password.text; - final hasPassword = passwordText.isNotEmpty; - var ip = await Ipify.ipv4(); - if(hasPassword) { - ip = aes256Encrypt(ip, passwordText); - } - - final supabase = Supabase.instance.client; - final hosts = supabase.from("hosting"); - final 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; - }catch(error) { - published.value = false; - }finally { - _publishingSemaphore.release(); - } - } - - Future discardServer() async { - try { - _publishingSemaphore.acquire(); - if(!published.value) { - return; - } - - final supabase = Supabase.instance.client; - await supabase.from("hosting") - .delete() - .match({'id': uuid}); - published.value = false; - }catch(_) { - published.value = true; - }finally { - _publishingSemaphore.release(); - } - } } \ No newline at end of file diff --git a/gui/lib/src/messenger/implementation/version.dart b/gui/lib/src/messenger/implementation/version.dart new file mode 100644 index 0000000..27297cd --- /dev/null +++ b/gui/lib/src/messenger/implementation/version.dart @@ -0,0 +1,463 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.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/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/file_selector.dart'; +import 'package:universal_disk_space/universal_disk_space.dart'; +import 'package:windows_taskbar/windows_taskbar.dart'; + +class AddVersionDialog extends StatefulWidget { + final bool closable; + const AddVersionDialog({Key? key, required this.closable}) : super(key: key); + + @override + State createState() => _AddVersionDialogState(); +} + +class _AddVersionDialogState extends State { + final GameController _gameController = Get.find(); + final TextEditingController _pathController = TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + final GlobalKey _formFieldKey = GlobalKey(); + + final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form); + final Rx<_BuildSource> _source = Rx(_BuildSource.githubArchive); + final Rxn _build = Rxn(); + final RxnInt _timeLeft = RxnInt(); + final Rxn _progress = Rxn(); + + late DiskSpace _diskSpace; + late Future> _fetchFuture; + late Future _diskFuture; + + Isolate? _isolate; + SendPort? _downloadPort; + Object? _error; + StackTrace? _stackTrace; + + @override + void initState() { + _fetchFuture = compute(fetchBuilds, null); + _diskSpace = DiskSpace(); + _diskFuture = _diskSpace.scan() + .then((_) => _updateFormDefaults()); + super.initState(); + } + + @override + void dispose() { + _pathController.dispose(); + _cancelDownload(); + super.dispose(); + } + + void _cancelDownload() { + Process.run('${assetsDirectory.path}\\build\\stop.bat', []); + _downloadPort?.send(kStopBuildDownloadSignal); + _isolate?.kill(priority: Isolate.immediate); + } + + @override + Widget build(BuildContext context) => Form( + key: _formKey, + child: Obx(() { + switch(_status.value){ + case _DownloadStatus.form: + return FutureBuilder( + future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture), + builder: (context, snapshot) { + if (snapshot.hasError) { + WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace)); + } + + final data = snapshot.data; + if (data == null) { + return ProgressDialog( + text: translations.fetchingBuilds, + showButton: widget.closable, + onStop: () => Navigator.of(context).pop() + ); + } + + return Obx(() => FormDialog( + content: _buildFormBody(data), + buttons: _formButtons + )); + } + ); + case _DownloadStatus.downloading: + case _DownloadStatus.extracting: + return GenericDialog( + header: _progressBody, + buttons: _stopButton + ); + case _DownloadStatus.error: + return ErrorDialog( + exception: _error ?? Exception(translations.unknownError), + stackTrace: _stackTrace, + errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString()) + ); + case _DownloadStatus.done: + return InfoDialog( + text: translations.downloadedVersion + ); + } + }) + ); + + List get _formButtons => [ + if(widget.closable) + DialogButton(type: ButtonType.secondary), + DialogButton( + text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download, + type: widget.closable ? ButtonType.primary : ButtonType.only, + color: FluentTheme.of(context).accentColor, + onTap: () => _startDownload(context), + ) + ]; + + void _startDownload(BuildContext context) async { + try { + final topResult = _formKey.currentState?.validate(); + if(topResult != true) { + return; + } + + final fieldResult = _formFieldKey.currentState?.validate(); + if(fieldResult != true) { + return; + } + + final build = _build.value; + if(build == null){ + return; + } + + final source = _source.value; + if(source == _BuildSource.local) { + Navigator.of(context).pop(); + _addFortniteVersion(build); + return; + } + + _status.value = _DownloadStatus.downloading; + final communicationPort = ReceivePort(); + communicationPort.listen((message) { + if(message is FortniteBuildDownloadProgress) { + _onProgress(build, message.progress, message.minutesLeft, message.extracting); + }else if(message is SendPort) { + _downloadPort = message; + }else { + _onDownloadError(message, null); + } + }); + final options = FortniteBuildDownloadOptions( + build, + Directory(_pathController.text), + communicationPort.sendPort + ); + final errorPort = ReceivePort(); + errorPort.listen((message) => _onDownloadError(message, null)); + _isolate = await Isolate.spawn( + downloadArchiveBuild, + options, + onError: errorPort.sendPort, + errorsAreFatal: true + ); + } catch (exception, stackTrace) { + _onDownloadError(exception, stackTrace); + } + } + + Future _onDownloadComplete(FortniteBuild build) async { + if (!mounted) { + return; + } + + _status.value = _DownloadStatus.done; + WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); + _addFortniteVersion(build); + } + + void _addFortniteVersion(FortniteBuild build) { + WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion( + content: build.version, + location: Directory(_pathController.text) + ))); + } + + void _onDownloadError(Object? error, StackTrace? stackTrace) { + _cancelDownload(); + if (!mounted) { + return; + } + + _status.value = _DownloadStatus.error; + WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); + _error = error; + _stackTrace = stackTrace; + } + + void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) { + if (!mounted) { + return; + } + + if(progress >= 100 && extracting) { + _onDownloadComplete(build); + return; + } + + _status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading; + if(progress >= 0) { + WindowsTaskbar.setProgress(progress.round(), 100); + } + + _timeLeft.value = timeLeft; + _progress.value = progress; + } + + Widget get _progressBody { + final timeLeft = _timeLeft.value; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + _status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting, + style: FluentTheme.maybeOf(context)?.typography.body, + textAlign: TextAlign.start, + ), + ), + + const SizedBox( + height: 8.0, + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translations.buildProgress((_progress.value ?? 0).round()), + style: FluentTheme.maybeOf(context)?.typography.body, + ), + + if(timeLeft != null) + Text( + translations.timeLeft(timeLeft), + style: FluentTheme.maybeOf(context)?.typography.body, + ) + ], + ), + + const SizedBox( + height: 8.0, + ), + + SizedBox( + width: double.infinity, + child: ProgressBar(value: (_progress.value ?? 0).toDouble()) + ), + + const SizedBox( + height: 8.0, + ) + ], + ); + } + + Widget _buildFormBody(List builds) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSourceSelector(), + + const SizedBox( + height: 16.0 + ), + + _buildBuildSelector(builds), + + FileSelector( + label: translations.gameFolderTitle, + placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder, + windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle, + controller: _pathController, + validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination, + folder: true + ), + + const SizedBox( + height: 16.0 + ) + ], + ); + + String? _checkGameFolder(text) { + if (text == null || text.isEmpty) { + return translations.emptyGamePath; + } + + final directory = Directory(text); + if (!directory.existsSync()) { + return translations.directoryDoesNotExist; + } + + if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) { + return translations.missingShippingExe; + } + + return null; + } + + String? _checkDownloadDestination(text) { + if (text == null || text.isEmpty) { + return translations.invalidDownloadPath; + } + + return null; + } + + Widget _buildBuildSelector(List builds) => InfoLabel( + label: translations.build, + child: FormField( + key: _formFieldKey, + validator: (data) => _checkBuild(data), + builder: (formContext) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ComboBox( + placeholder: Text(translations.selectBuild), + isExpanded: true, + items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(), + value: _build.value, + onChanged: (value) { + if(value == null){ + return; + } + + _build.value = value; + formContext.didChange(value); + formContext.validate(); + _updateFormDefaults(); + } + ), + if(formContext.hasError) + const SizedBox(height: 4.0), + if(formContext.hasError) + Text( + formContext.errorText ?? "", + style: TextStyle( + color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness) + ), + ), + SizedBox( + height: formContext.hasError ? 8.0 : 16.0 + ), + ], + ) + ) + ); + + String? _checkBuild(FortniteBuild? data) { + if(data == null) { + return translations.selectBuild; + } + + final versions = _gameController.versions.value; + if (versions.any((element) => data.version == element.content)) { + return translations.versionAlreadyExists; + } + + return null; + } + + ComboBoxItem _buildBuildItem(FortniteBuild element) => ComboBoxItem( + value: element, + child: Text(element.version.toString()) + ); + + Widget _buildSourceSelector() => InfoLabel( + label: translations.source, + child: ComboBox<_BuildSource>( + placeholder: Text(translations.selectBuild), + isExpanded: true, + items: _BuildSource.values.map((entry) => _buildSourceItem(entry)).toList(), + value: _source.value, + onChanged: (value) { + if(value == null){ + return; + } + + _source.value = value; + _updateFormDefaults(); + } + ) + ); + + ComboBoxItem<_BuildSource> _buildSourceItem(_BuildSource element) => ComboBoxItem<_BuildSource>( + value: element, + child: Text(element.translatedName) + ); + + + List get _stopButton => [ + DialogButton( + text: translations.stopLoadingDialogAction, + type: ButtonType.only + ) + ]; + + Future _updateFormDefaults() async { + if(_source.value != _BuildSource.local && _build.value?.available != true) { + _build.value = null; + } + + if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) { + await _fetchFuture; + final bestDisk = _diskSpace.disks + .reduce((first, second) => first.availableSpace > second.availableSpace ? first : second); + final build = _build.value; + if(build == null){ + return; + } + + final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}"; + _pathController.text = pathText; + _pathController.selection = TextSelection.collapsed(offset: pathText.length); + } + + _formKey.currentState?.validate(); + } +} + +enum _DownloadStatus { + form, + downloading, + extracting, + error, + done +} + +enum _BuildSource { + local, + githubArchive; + + String get translatedName { + switch(this) { + case _BuildSource.local: + return translations.localBuild; + case _BuildSource.githubArchive: + return translations.githubArchive; + } + } +} + diff --git a/gui/lib/src/page/implementation/backend_page.dart b/gui/lib/src/page/implementation/backend_page.dart index 1b3981c..947a6e7 100644 --- a/gui/lib/src/page/implementation/backend_page.dart +++ b/gui/lib/src/page/implementation/backend_page.dart @@ -6,7 +6,9 @@ import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; +import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/util/keyboard.dart'; @@ -16,7 +18,10 @@ import 'package:reboot_launcher/src/widget/server_type_selector.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../dialog/implementation/data.dart'; +final GlobalKey backendTypeOverlayTargetKey = GlobalKey(); +final GlobalKey backendGameServerAddressOverlayTargetKey = GlobalKey(); +final GlobalKey backendUnrealEngineOverlayTargetKey = GlobalKey(); +final GlobalKey backendDetachedOverlayTargetKey = GlobalKey(); class BackendPage extends RebootPage { const BackendPage({Key? key}) : super(key: key); @@ -42,7 +47,7 @@ class _BackendPageState extends RebootPageState { final BackendController _backendController = Get.find(); InfoBarEntry? _infoBarEntry; - + @override void initState() { ServicesBinding.instance.keyboard.addHandler((keyEvent) { @@ -60,7 +65,7 @@ class _BackendPageState extends RebootPageState { }); super.initState(); } - + @override List get settings => [ _type, @@ -84,10 +89,13 @@ class _BackendPageState extends RebootPageState { ), title: Text(translations.matchmakerConfigurationAddressName), subtitle: Text(translations.matchmakerConfigurationAddressDescription), - content: TextFormBox( - placeholder: translations.matchmakerConfigurationAddressName, - controller: _backendController.gameServerAddress, - focusNode: _backendController.gameServerAddressFocusNode + content: OverlayTarget( + key: backendGameServerAddressOverlayTargetKey, + child: TextFormBox( + placeholder: translations.matchmakerConfigurationAddressName, + controller: _backendController.gameServerAddress, + focusNode: _backendController.gameServerAddressFocusNode + ), ) ); }); @@ -152,15 +160,18 @@ class _BackendPageState extends RebootPageState { const SizedBox( width: 16.0 ), - ToggleSwitch( - checked: _backendController.detached(), - onChanged: (value) => _backendController.detached.value = value + OverlayTarget( + key: backendDetachedOverlayTargetKey, + child: ToggleSwitch( + checked: _backendController.detached(), + onChanged: (value) => _backendController.detached.value = value + ), ), ], ) ); - }); - + }); + Widget get _unrealEngineConsoleKey => Obx(() { if(_backendController.type.value != ServerType.embedded) { return const SizedBox.shrink(); @@ -173,14 +184,18 @@ class _BackendPageState extends RebootPageState { title: Text(translations.settingsClientConsoleKeyName), subtitle: Text(translations.settingsClientConsoleKeyDescription), contentWidth: null, - content: Button( - onPressed: () { - _infoBarEntry = showInfoBar( - translations.clickKey, - loading: true - ); - }, - child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""), + content: OverlayTarget( + key: backendUnrealEngineOverlayTargetKey, + child: Button( + onPressed: () { + _infoBarEntry = showRebootInfoBar( + translations.clickKey, + loading: true, + duration: null + ); + }, + child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""), + ), ) ); }); @@ -221,7 +236,9 @@ class _BackendPageState extends RebootPageState { ), title: Text(translations.backendTypeName), subtitle: Text(translations.backendTypeDescription), - content: const ServerTypeSelector() + content: ServerTypeSelector( + overlayKey: backendTypeOverlayTargetKey + ) ); @override diff --git a/gui/lib/src/page/implementation/browser_page.dart b/gui/lib/src/page/implementation/browser_page.dart new file mode 100644 index 0000000..c802350 --- /dev/null +++ b/gui/lib/src/page/implementation/browser_page.dart @@ -0,0 +1,359 @@ + +import 'dart:async'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentUiIcons; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/backend_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; +import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/implementation/server.dart'; +import 'package:reboot_launcher/src/page/abstract/page.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/setting_tile.dart'; + +class BrowsePage extends RebootPage { + const BrowsePage({Key? key}) : super(key: key); + + @override + String get name => translations.browserName; + + @override + RebootPageType get type => RebootPageType.browser; + + @override + String get iconAsset => "assets/images/server_browser.png"; + + @override + bool hasButton(String? pageName) => false; + + @override + RebootPageState createState() => _BrowsePageState(); +} + +class _BrowsePageState extends RebootPageState { + final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); + final BackendController _backendController = Get.find(); + final TextEditingController _filterController = TextEditingController(); + final StreamController _filterControllerStream = StreamController.broadcast(); + + final Rx<_Filter> _filter = Rx(_Filter.all); + final Rx<_Sort> _sort = Rx(_Sort.timeDescending); + + @override + Widget build(BuildContext context) { + super.build(context); + return Obx(() { + final data = _hostingController.servers.value + ?.where((entry) => (kDebugMode || entry.id != _hostingController.uuid) && entry.discoverable) + .toSet(); + if(data == null || data.isEmpty == true) { + return _noServers; + } + + return _buildPageBody(data); + }); + } + + Widget get _noServers => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + translations.noServersAvailableTitle, + style: FluentTheme.of(context).typography.titleLarge, + ), + Text( + translations.noServersAvailableSubtitle, + style: FluentTheme.of(context).typography.body + ), + ], + ); + + Widget _buildPageBody(Set data) => StreamBuilder( + stream: _filterControllerStream.stream, + builder: (context, filterSnapshot) { + final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet(); + return Column( + children: [ + _searchBar, + const SizedBox( + height: 24, + ), + Row( + children: [ + _buildFilter(context), + const SizedBox( + width: 16.0 + ), + _buildSort(context), + ], + ), + const SizedBox( + height: 24, + ), + Expanded( + child: _buildPopulatedListBody(items) + ), + ], + ); + } + ); + + Widget _buildSort(BuildContext context) => Row( + children: [ + Icon( + fluentUiIcons.FluentIcons.arrow_sort_24_regular, + color: FluentTheme.of(context).resources.textFillColorDisabled + ), + const SizedBox(width: 4.0), + Text( + "Sort by: ", + style: TextStyle( + color: FluentTheme.of(context).resources.textFillColorDisabled + ), + ), + const SizedBox(width: 4.0), + Obx(() => SizedBox( + width: 230, + child: DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text( + _sort.value.translatedName, + textAlign: TextAlign.start + ), + title: const Spacer(), + items: _Sort.values.map((entry) => MenuFlyoutItem( + text: Text(entry.translatedName), + onPressed: () => _sort.value = entry + )).toList() + ), + )) + ], + ); + + Row _buildFilter(BuildContext context) { + return Row( + children: [ + Icon( + fluentUiIcons.FluentIcons.filter_24_regular, + color: FluentTheme.of(context).resources.textFillColorDisabled + ), + const SizedBox(width: 4.0), + Text( + "Filter by: ", + style: TextStyle( + color: FluentTheme.of(context).resources.textFillColorDisabled + ), + ), + const SizedBox(width: 4.0), + Obx(() => SizedBox( + width: 125, + child: DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text( + _filter.value.translatedName, + textAlign: TextAlign.start + ), + title: const Spacer(), + items: _Filter.values.map((entry) => MenuFlyoutItem( + text: Text(entry.translatedName), + onPressed: () => _filter.value = entry + )).toList() + ), + )) + ], + ); + } + + Widget _buildPopulatedListBody(Set items) => Obx(() { + final filter = _filter.value; + final sorted = items.where((element) { + switch(filter) { + case _Filter.all: + return true; + case _Filter.accessible: + return element.password == null; + case _Filter.playable: + return _gameController.getVersionByName(element.version) != null; + } + }).toList(); + final sort = _sort.value; + sorted.sort((first, second) { + switch(sort) { + case _Sort.timeAscending: + return first.timestamp.compareTo(second.timestamp); + case _Sort.timeDescending: + return second.timestamp.compareTo(first.timestamp); + case _Sort.nameAscending: + return first.name.compareTo(second.name); + case _Sort.nameDescending: + return second.name.compareTo(first.name); + } + }); + if(sorted.isEmpty) { + return _noServersByQuery; + } + + return ListView.builder( + itemCount: sorted.length, + itemBuilder: (context, index) { + final entry = sorted.elementAt(index); + final hasPassword = entry.password != null; + return SettingTile( + icon: Icon( + hasPassword ? FluentIcons.lock : FluentIcons.globe + ), + title: Text("${_formatName(entry)} • ${entry.author}"), + subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"), + content: Button( + onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry), + child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp), + ) + ); + } + ); + }); + + Widget get _noServersByQuery => Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + translations.noServersAvailableByQueryTitle, + style: FluentTheme.of(context).typography.titleLarge, + ), + Text( + translations.noServersAvailableByQuerySubtitle, + style: FluentTheme.of(context).typography.body + ), + ], + ); + + bool _isValidItem(FortniteServer entry, String? filter) => + filter == null || filter.isEmpty || _filterServer(entry, filter); + + bool _filterServer(FortniteServer element, String filter) { + filter = filter.toLowerCase(); + + final uri = Uri.tryParse(filter); + if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) { + return true; + } + + return element.id.toLowerCase().contains(filter.toLowerCase()) + || element.name.toLowerCase().contains(filter) + || element.author.toLowerCase().contains(filter) + || element.description.toLowerCase().contains(filter); + } + + Widget get _searchBar => Align( + alignment: Alignment.centerLeft, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 350 + ), + child: TextBox( + placeholder: translations.findServer, + controller: _filterController, + autofocus: true, + onChanged: (value) => _filterControllerStream.add(value), + suffix: _searchBarIcon, + ), + ), + ); + + Widget get _searchBarIcon => Button( + onPressed: _filterController.text.isEmpty ? null : () { + _filterController.clear(); + _filterControllerStream.add(""); + }, + style: ButtonStyle( + backgroundColor: ButtonState.all(Colors.transparent), + shape: ButtonState.all(Border()) + ), + child: _searchBarIconData + ); + + Widget get _searchBarIconData { + final color = FluentTheme.of(context).resources.textFillColorPrimary; + if (_filterController.text.isNotEmpty) { + return Icon( + FluentIcons.clear, + size: 8.0, + color: color + ); + } + + return Transform.flip( + flipX: true, + child: Icon( + FluentIcons.search, + size: 12.0, + color: color + ), + ); + } + + String _formatName(FortniteServer server) { + final result = server.name; + return result.isEmpty ? translations.defaultServerName : result; + } + + String _formatDescription(FortniteServer server) { + final result = server.description; + return result.isEmpty ? translations.defaultServerDescription : result; + } + + String _formatVersion(FortniteServer server) => "Fortnite ${server.version.toString()}"; + + @override + Widget? get button => null; + + @override + List get settings => []; +} + +enum _Filter { + all, + accessible, + playable; + + String get translatedName { + switch(this) { + case _Filter.all: + return translations.all; + case _Filter.accessible: + return translations.accessible; + case _Filter.playable: + return translations.playable; + } + } +} + +enum _Sort { + timeAscending, + timeDescending, + nameAscending, + nameDescending; + + String get translatedName { + switch(this) { + case _Sort.timeAscending: + return translations.timeAscending; + case _Sort.timeDescending: + return translations.timeDescending; + case _Sort.nameAscending: + return translations.nameAscending; + case _Sort.nameDescending: + return translations.nameDescending; + } + } +} \ No newline at end of file diff --git a/gui/lib/src/page/implementation/home_page.dart b/gui/lib/src/page/implementation/home_page.dart index c944fa3..8655e0d 100644 --- a/gui/lib/src/page/implementation/home_page.dart +++ b/gui/lib/src/page/implementation/home_page.dart @@ -12,15 +12,14 @@ import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/controller/update_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/dialog/implementation/dll.dart'; -import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; +import 'package:reboot_launcher/src/messenger/implementation/dll.dart'; +import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart'; import 'package:reboot_launcher/src/page/pages.dart'; -import 'package:reboot_launcher/src/util/dll.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/translations.dart'; @@ -29,9 +28,12 @@ import 'package:reboot_launcher/src/widget/profile_tile.dart'; import 'package:reboot_launcher/src/widget/title_bar.dart'; import 'package:window_manager/window_manager.dart'; -import 'info_page.dart'; +final GlobalKey profileOverlayKey = GlobalKey(); class HomePage extends StatefulWidget { + static const double kDefaultPadding = 12.0; + static const double kTitleBarHeight = 32; + const HomePage({Key? key}) : super(key: key); @override @@ -39,16 +41,14 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State with WindowListener, AutomaticKeepAliveClientMixin { - static const double _kDefaultPadding = 12.0; - final BackendController _backendController = Get.find(); final HostingController _hostingController = Get.find(); final SettingsController _settingsController = Get.find(); - final UpdateController _updateController = Get.find(); final GlobalKey _searchKey = GlobalKey(); final FocusNode _searchFocusNode = FocusNode(); final TextEditingController _searchController = TextEditingController(); final RxBool _focused = RxBool(true); + final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value); @override bool get wantKeepAlive => true; @@ -56,7 +56,9 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA @override void initState() { super.initState(); + windowManager.setPreventClose(true); windowManager.addListener(this); + _syncPageViewWithNavigator(); WidgetsBinding.instance.addPostFrameCallback((_) { _checkUpdates(); _initAppLink(); @@ -64,6 +66,18 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA }); } + void _syncPageViewWithNavigator() { + var lastPage = pageIndex.value; + pageIndex.listen((index) { + if(index == lastPage) { + return; + } + + lastPage = index; + _pageController.jumpToPage(index); + }); + } + void _initAppLink() async { final appLinks = AppLinks(); final initialUrl = await appLinks.getInitialLink(); @@ -78,9 +92,9 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA final uuid = uri.host; final server = _hostingController.findServerById(uuid); if(server != null) { - _backendController.joinServer(_hostingController.uuid, server); + _backendController.joinServerInteractive(_hostingController.uuid, server); }else { - showInfoBar( + showRebootInfoBar( translations.noServerFound, duration: infoBarLongDuration, severity: InfoBarSeverity.error @@ -100,10 +114,9 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA return; } - var oldOwner = _backendController.gameServerOwner.value; - _backendController.joinLocalHost(); - WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar( - oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner), + _backendController.joinLocalhost(); + WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar( + translations.serverNoLongerAvailableUnnamed, severity: InfoBarSeverity.warning, duration: infoBarLongDuration )); @@ -114,27 +127,34 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA } void _checkUpdates() { - _updateController.notifyLauncherUpdate(); + _settingsController.notifyLauncherUpdate(); if(!dllsDirectory.existsSync()) { dllsDirectory.createSync(recursive: true); } for(final injectable in InjectableDll.values) { - downloadCriticalDllInteractive( - injectable.path, - silent: true - ); + final (file, custom) = _settingsController.getInjectableData(injectable); + if(!custom) { + _settingsController.downloadCriticalDllInteractive( + file.path, + silent: true + ); + } } watchDlls().listen((filePath) => showDllDeletedDialog(() { - downloadCriticalDllInteractive(filePath); + _settingsController.downloadCriticalDllInteractive(filePath); })); } @override - void onWindowClose() { - exit(0); // Force closing + void onWindowClose() async { + try { + await _hostingController.discardServer(); + }finally { + exit(0); // Force closing + } } @override @@ -153,7 +173,7 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA @override void onWindowBlur() { - _focused.value = false; + _focused.value = !_focused.value; } @override @@ -218,137 +238,364 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA _focused.value = true; } + @override + void onWindowEvent(String eventName) { + if(eventName != "move") { + WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}")); + } + } + @override Widget build(BuildContext context) { super.build(context); _settingsController.language.value; loadTranslations(context); - // InfoPage.initInfoTiles(); return Obx(() { - return NavigationPaneTheme( - data: NavigationPaneThemeData( - backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93), - ), - child: NavigationView( - paneBodyBuilder: (pane, body) => _PaneBody( - padding: _kDefaultPadding, - controller: pagesController, - body: body - ), - appBar: NavigationAppBar( - height: 32, - title: _draggableArea, - actions: WindowTitleBar(focused: _focused()), - leading: _backButton, - automaticallyImplyLeading: false, - ), - pane: NavigationPane( - selected: pageIndex.value, - onChanged: (index) { - final lastPageIndex = pageIndex.value; - if(lastPageIndex != index) { - pageIndex.value = index; - }else if(pageStack.isNotEmpty) { - Navigator.of(pageKey.currentContext!).pop(); - final element = pageStack.removeLast(); - appStack.remove(element); - pagesController.add(null); - } - }, - menuButton: const SizedBox(), - displayMode: PaneDisplayMode.open, - items: _items, - customPane: _CustomPane(_settingsController), - header: const ProfileWidget(), - autoSuggestBox: _autoSuggestBox, - indicator: const StickyNavigationIndicator( - duration: Duration(milliseconds: 500), - curve: Curves.easeOut, - indicatorSize: 3.25 + return Container( + color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: HomePage.kTitleBarHeight, + child: Row( + children: [ + _backButton, + Expanded(child: _draggableArea), + WindowTitleBar(focused: _focused()) + ], + ) + ), + Expanded( + child: Navigator( + key: appNavigatorKey, + onPopPage: (page, data) => false, + pages: [ + MaterialPage( + child: Overlay( + key: appOverlayKey, + initialEntries: [ + OverlayEntry( + maintainState: true, + builder: (context) => Row( + children: [ + _buildLateralView(), + _buildBody() + ], + ) + ) + ], + ), ) - ), - contentShape: const RoundedRectangleBorder(), - onOpenSearch: () => _searchFocusNode.requestFocus(), - transitionBuilder: (child, animation) => child - ) + ], + ) + ) + ], + ), ); }); } + Widget _buildBody() { + return Expanded( + child: Padding( + padding: EdgeInsets.only( + left: HomePage.kDefaultPadding, + right: HomePage.kDefaultPadding * 2, + top: HomePage.kDefaultPadding, + bottom: HomePage.kDefaultPadding * 2 + ), + child: Column( + children: [ + Expanded( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 1000 + ), + child: Center( + child: Column( + children: [ + _buildBodyHeader(), + const SizedBox(height: 24.0), + Expanded( + child: Stack( + fit: StackFit.loose, + children: [ + _buildBodyContent(), + InfoBarArea( + key: infoBarAreaKey + ) + ], + ) + ), + ], + ), + ), + ), + ) + ], + ) + ), + ); + } + + Widget _buildBodyContent() => PageView.builder( + controller: _pageController, + itemBuilder: (context, index) => Navigator( + onPopPage: (page, data) => true, + observers: [ + _NestedPageObserver( + onChanged: (routeName) { + if(routeName != null) { + pageIndex.refresh(); + addSubPageToStack(routeName); + pagesController.add(null); + } + } + ) + ], + pages: [ + MaterialPage( + child: KeyedSubtree( + key: getPageKeyByIndex(index), + child: pages[index] + ) + ) + ], + ), + itemCount: pages.length + ); + + Widget _buildBodyHeader() { + final themeMode = _settingsController.themeMode.value; + final inactiveColor = themeMode == ThemeMode.dark + || (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100]; + return Align( + alignment: Alignment.centerLeft, + child: StreamBuilder( + stream: pagesController.stream, + builder: (context, _) { + final elements = []; + elements.add(_buildBodyHeaderRootPage(inactiveColor)); + for(var i = pageStack.length - 1; i >= 0; i--) { + var innerPage = pageStack.elementAt(i); + innerPage = innerPage.substring(innerPage.indexOf("_") + 1); + elements.add(_buildBodyHeaderPageSeparator(inactiveColor)); + elements.add(_buildBodyHeaderNestedPage(innerPage, i, inactiveColor)); + } + + return Text.rich( + TextSpan( + children: elements + ), + style: TextStyle( + fontSize: 32.0, + fontWeight: FontWeight.w600 + ), + ); + } + ), + ); + } + + TextSpan _buildBodyHeaderRootPage(Color inactiveColor) => TextSpan( + text: pages[pageIndex.value].name, + recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () { + if(inDialog) { + return; + } + + for(var i = 0; i < pageStack.length; i++) { + Navigator.of(pageKey.currentContext!).pop(); + final element = pageStack.removeLast(); + appStack.remove(element); + } + + pagesController.add(null); + }) : null, + style: TextStyle( + color: pageStack.isNotEmpty ? inactiveColor : null + ) + ); + + TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan( + text: " > ", + style: TextStyle( + color: inactiveColor + ) + ); + + TextSpan _buildBodyHeaderNestedPage(String nestedPageName, int nestedPageIndex, Color inactiveColor) => TextSpan( + text: nestedPageName, + recognizer: nestedPageIndex == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () { + if(inDialog) { + return; + } + + for(var j = 0; j < nestedPageIndex - 1; j++) { + Navigator.of(pageKey.currentContext!).pop(); + final element = pageStack.removeLast(); + appStack.remove(element); + } + pagesController.add(null); + }), + style: TextStyle( + color: nestedPageIndex == pageStack.length - 1 ? null : inactiveColor + ) + ); + + Widget _buildLateralView() => SizedBox( + width: 310, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileWidget( + overlayKey: profileOverlayKey + ), + _autoSuggestBox, + const SizedBox(height: 12.0), + _buildNavigationTrail() + ], + ), + ); + + Widget _buildNavigationTrail() => Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0 + ), + child: Scrollbar( + child: ListView.separated( + primary: true, + itemCount: pages.length, + separatorBuilder: (context, index) => const SizedBox( + height: 4.0 + ), + itemBuilder: (context, index) => _buildNavigationItem(pages[index]), + ), + ), + ) + ); + + Widget _buildNavigationItem(RebootPage page) { + final index = page.type.index; + return OverlayTarget( + key: getOverlayTargetKeyByPage(index), + child: HoverButton( + onPressed: () { + final lastPageIndex = pageIndex.value; + if(lastPageIndex != index) { + pageIndex.value = index; + }else if(pageStack.isNotEmpty) { + Navigator.of(pageKey.currentContext!).pop(); + final element = pageStack.removeLast(); + appStack.remove(element); + pagesController.add(null); + } + }, + builder: (context, states) => Obx(() => Container( + height: 36, + decoration: BoxDecoration( + color: ButtonThemeData.uncheckedInputColor( + FluentTheme.of(context), + pageIndex.value == index ? {ButtonStates.hovering} : states, + transparentWhenNone: true, + ), + borderRadius: BorderRadius.all(Radius.circular(6.0)) + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0 + ), + child: Row( + children: [ + SizedBox.square( + dimension: 24, + child: Image.asset(page.iconAsset) + ), + const SizedBox(width: 12.0), + Text(page.name) + ], + ), + ), + )), + ), + ); + } + Widget get _backButton => StreamBuilder( stream: pagesController.stream, builder: (context, _) => Button( - style: ButtonStyle( - padding: ButtonState.all(const EdgeInsets.only(top: 6.0)), - backgroundColor: ButtonState.all(Colors.transparent), - shape: ButtonState.all(Border()) - ), - onPressed: appStack.isEmpty && !inDialog ? null : () { - if(inDialog) { - Navigator.of(appKey.currentContext!).pop(); - }else { - final lastPage = appStack.removeLast(); - pageStack.remove(lastPage); - if (lastPage is int) { - hitBack = true; - pageIndex.value = lastPage; - } else { - Navigator.of(pageKey.currentContext!).pop(); - } + style: ButtonStyle( + padding: ButtonState.all(const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 16.0 + )), + backgroundColor: ButtonState.all(Colors.transparent), + shape: ButtonState.all(Border()) + ), + onPressed: appStack.isEmpty && !inDialog ? null : () { + if(inDialog) { + Navigator.of(appNavigatorKey.currentContext!).pop(); + }else { + final lastPage = appStack.removeLast(); + pageStack.remove(lastPage); + if (lastPage is int) { + hitBack = true; + pageIndex.value = lastPage; + } else { + Navigator.of(pageKey.currentContext!).pop(); } - pagesController.add(null); - }, - child: const Icon(FluentIcons.back, size: 12.0), - ) + } + pagesController.add(null); + }, + child: const Icon(FluentIcons.back, size: 12.0), + ) ); GestureDetector get _draggableArea => GestureDetector( onDoubleTap: appWindow.maximizeOrRestore, - onHorizontalDragStart: (_) => appWindow.startDragging(), - onVerticalDragStart: (_) => appWindow.startDragging() + onHorizontalDragStart: (_) => windowManager.startDragging(), + onVerticalDragStart: (_) => windowManager.startDragging() ); - Widget get _autoSuggestBox => Obx(() { - final firstRun = _settingsController.firstRun.value; - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0 + Widget get _autoSuggestBox => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0 + ), + child: AutoSuggestBox( + key: _searchKey, + controller: _searchController, + placeholder: translations.find, + focusNode: _searchFocusNode, + selectionHeightStyle: BoxHeightStyle.max, + itemBuilder: (context, item) => ListTile( + onPressed: () { + pageIndex.value = item.value.pageIndex; + _searchController.clear(); + _searchFocusNode.unfocus(); + }, + leading: item.child, + title: Text( + item.value.name, + overflow: TextOverflow.clip, + maxLines: 1 + ) ), - child: AutoSuggestBox( - key: _searchKey, - controller: _searchController, - enabled: !firstRun, - placeholder: translations.find, - focusNode: _searchFocusNode, - selectionHeightStyle: BoxHeightStyle.max, - itemBuilder: (context, item) => ListTile( - onPressed: () { - pageIndex.value = item.value.pageIndex; - _searchController.clear(); - _searchFocusNode.unfocus(); - }, - leading: item.child, - title: Text( - item.value.name, - overflow: TextOverflow.clip, - maxLines: 1 - ) - ), - items: _suggestedItems, - autofocus: true, - trailingIcon: IgnorePointer( - child: IconButton( - onPressed: () {}, - icon: Transform.flip( - flipX: true, - child: const Icon(FluentIcons.search) - ), - ) - ), - ) - ); - }); + items: _suggestedItems, + autofocus: true, + trailingIcon: IgnorePointer( + child: IconButton( + onPressed: () {}, + icon: Transform.flip( + flipX: true, + child: const Icon(FluentIcons.search) + ), + ) + ), + ) + ); List> get _suggestedItems => pages.mapMany((page) { final pageIcon = SizedBox.square( @@ -367,282 +614,6 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA )); return results; }).toList(); - - List get _items => pages.map((page) => _createItem(page)).toList(); - - NavigationPaneItem _createItem(RebootPage page) => PaneItem( - title: Text(page.name), - icon: SizedBox.square( - dimension: 24, - child: Image.asset(page.iconAsset) - ), - body: page - ); -} - -class _PaneBody extends StatefulWidget { - const _PaneBody({ - required this.padding, - required this.controller, - required this.body - }); - - final double padding; - final StreamController controller; - final Widget? body; - - @override - State<_PaneBody> createState() => _PaneBodyState(); -} - -class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin { - final SettingsController _settingsController = Get.find(); - final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value); - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - var lastPage = pageIndex.value; - pageIndex.listen((index) { - if(index == lastPage) { - return; - } - - lastPage = index; - _pageController.jumpToPage(index); - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - final themeMode = _settingsController.themeMode.value; - final inactiveColor = themeMode == ThemeMode.dark - || (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100]; - return Padding( - padding: EdgeInsets.only( - left: widget.padding, - right: widget.padding * 2, - top: widget.padding, - bottom: widget.padding * 2 - ), - child: Column( - children: [ - Expanded( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 1000 - ), - child: Center( - child: Column( - children: [ - Align( - alignment: Alignment.centerLeft, - child: StreamBuilder( - stream: widget.controller.stream, - builder: (context, _) { - final elements = []; - elements.add(TextSpan( - text: pages[pageIndex.value].name, - recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () { - if(inDialog) { - return; - } - - for(var i = 0; i < pageStack.length; i++) { - Navigator.of(pageKey.currentContext!).pop(); - final element = pageStack.removeLast(); - appStack.remove(element); - } - - widget.controller.add(null); - }) : null, - style: TextStyle( - color: pageStack.isNotEmpty ? inactiveColor : null - ) - )); - for(var i = pageStack.length - 1; i >= 0; i--) { - var innerPage = pageStack.elementAt(i); - innerPage = innerPage.substring(innerPage.indexOf("_") + 1); - elements.add(TextSpan( - text: " > ", - style: TextStyle( - color: inactiveColor - ) - )); - elements.add(TextSpan( - text: innerPage, - recognizer: i == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () { - if(inDialog) { - return; - } - - for(var j = 0; j < i - 1; j++) { - Navigator.of(pageKey.currentContext!).pop(); - final element = pageStack.removeLast(); - appStack.remove(element); - } - widget.controller.add(null); - }), - style: TextStyle( - color: i == pageStack.length - 1 ? null : inactiveColor - ) - )); - } - - return Text.rich( - TextSpan( - children: elements - ), - style: TextStyle( - fontSize: 32.0, - fontWeight: FontWeight.w600 - ), - ); - } - ), - ), - const SizedBox(height: 24.0), - Expanded( - child: Stack( - fit: StackFit.loose, - children: [ - PageView.builder( - controller: _pageController, - itemBuilder: (context, index) => Navigator( - onPopPage: (page, data) => true, - observers: [ - _NestedPageObserver( - onChanged: (routeName) { - if(routeName != null) { - pageIndex.refresh(); - addSubPageToStack(routeName); - widget.controller.add(null); - } - } - ) - ], - pages: [ - MaterialPage( - child: KeyedSubtree( - key: getPageKeyByIndex(index), - child: widget.body ?? const SizedBox.shrink() - ) - ) - ], - ), - itemCount: pages.length - ), - InfoBarArea( - key: infoBarAreaKey - ) - ], - ) - ), - ], - ), - ), - ), - ) - ], - ) - ); - } -} - -class _CustomPane extends NavigationPaneWidget { - final SettingsController settingsController; - _CustomPane(this.settingsController); - - @override - Widget build(BuildContext context, NavigationPaneWidgetData data) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - data.appBar, - Expanded( - child: Navigator( - key: appKey, - onPopPage: (page, data) => false, - pages: [ - MaterialPage( - child: Row( - children: [ - SizedBox( - width: 310, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - data.pane.header ?? const SizedBox.shrink(), - data.pane.autoSuggestBox ?? const SizedBox.shrink(), - const SizedBox(height: 12.0), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0 - ), - child: Scrollbar( - controller: data.scrollController, - child: ListView.separated( - controller: data.scrollController, - itemCount: data.pane.items.length, - separatorBuilder: (context, index) => const SizedBox( - height: 4.0 - ), - itemBuilder: (context, index) { - final item = data.pane.items[index] as PaneItem; - return Obx(() { - final firstRun = settingsController.firstRun.value; - return HoverButton( - onPressed: firstRun ? null : () => data.pane.onChanged?.call(index), - builder: (context, states) => Container( - height: 36, - decoration: BoxDecoration( - color: ButtonThemeData.uncheckedInputColor( - FluentTheme.of(context), - item == data.pane.selectedItem ? {ButtonStates.hovering} : states, - transparentWhenNone: true, - ), - borderRadius: BorderRadius.all(Radius.circular(6.0)) - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0 - ), - child: Row( - children: [ - data.pane.indicator ?? const SizedBox.shrink(), - item.icon, - const SizedBox(width: 12.0), - item.title ?? const SizedBox.shrink() - ], - ), - ), - ), - ); - }); - }, - ), - ), - ), - ) - ], - ), - ), - Expanded( - child: data.content - ) - ], - ) - ) - ], - ), - - ) - ], - ); } class _NestedPageObserver extends NavigatorObserver { diff --git a/gui/lib/src/page/implementation/server_host_page.dart b/gui/lib/src/page/implementation/host_page.dart similarity index 71% rename from gui/lib/src/page/implementation/server_host_page.dart rename to gui/lib/src/page/implementation/host_page.dart index a327cd8..cc14d21 100644 --- a/gui/lib/src/page/implementation/server_host_page.dart +++ b/gui/lib/src/page/implementation/host_page.dart @@ -10,11 +10,10 @@ 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/settings_controller.dart'; -import 'package:reboot_launcher/src/controller/update_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/dialog/implementation/data.dart'; -import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; +import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/util/translations.dart'; @@ -23,7 +22,13 @@ import 'package:reboot_launcher/src/widget/game_start_button.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/version_selector_tile.dart'; -import '../../util/checks.dart'; +final GlobalKey hostVersionOverlayTargetKey = GlobalKey(); +final GlobalKey hostInfoOverlayTargetKey = GlobalKey(); +final GlobalKey hostInfoNameOverlayTargetKey = GlobalKey(); +final GlobalKey hostInfoDescriptionOverlayTargetKey = GlobalKey(); +final GlobalKey hostInfoPasswordOverlayTargetKey = GlobalKey(); +final GlobalKey hostShareOverlayTargetKey = GlobalKey(); +final GlobalKey hostInfoTileKey = GlobalKey(); class HostPage extends RebootPage { const HostPage({Key? key}) : super(key: key); @@ -47,7 +52,6 @@ class HostPage extends RebootPage { class _HostingPageState extends RebootPageState { final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); - final UpdateController _updateController = Get.find(); final SettingsController _settingsController = Get.find(); late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); @@ -67,39 +71,32 @@ class _HostingPageState extends RebootPageState { @override Widget get button => LaunchButton( - host: true, - startLabel: translations.startHosting, - stopLabel: translations.stopHosting + host: true, + startLabel: translations.startHosting, + stopLabel: translations.stopHosting ); @override List get settings => [ _information, - versionSelectSettingTile, + buildVersionSelector( + key: hostVersionOverlayTargetKey + ), _options, _internalFiles, _share, _resetDefaults ]; - SettingTile get _resetDefaults => SettingTile( - icon: Icon( - FluentIcons.arrow_reset_24_regular - ), - title: Text(translations.hostResetName), - subtitle: Text(translations.hostResetDescription), - content: Button( - onPressed: () => showResetDialog(_hostingController.reset), - child: Text(translations.hostResetContent), - ) - ); SettingTile get _information => SettingTile( + key: hostInfoTileKey, icon: Icon( FluentIcons.info_24_regular ), title: Text(translations.hostGameServerName), subtitle: Text(translations.hostGameServerDescription), + overlayKey: hostInfoOverlayTargetKey, children: [ SettingTile( icon: Icon( @@ -107,10 +104,14 @@ class _HostingPageState extends RebootPageState { ), title: Text(translations.hostGameServerNameName), subtitle: Text(translations.hostGameServerNameDescription), - content: TextFormBox( - placeholder: translations.hostGameServerNameName, - controller: _hostingController.name, - onChanged: (_) => _updateServer() + content: OverlayTarget( + key: hostInfoNameOverlayTargetKey, + child: TextFormBox( + placeholder: translations.hostGameServerNameName, + controller: _hostingController.name, + focusNode: _hostingController.nameFocusNode, + onChanged: (_) => _updateServer() + ), ) ), SettingTile( @@ -119,10 +120,14 @@ class _HostingPageState extends RebootPageState { ), title: Text(translations.hostGameServerDescriptionName), subtitle: Text(translations.hostGameServerDescriptionDescription), - content: TextFormBox( - placeholder: translations.hostGameServerDescriptionName, - controller: _hostingController.description, - onChanged: (_) => _updateServer() + content: OverlayTarget( + key: hostInfoDescriptionOverlayTargetKey, + child: TextFormBox( + placeholder: translations.hostGameServerDescriptionName, + controller: _hostingController.description, + focusNode: _hostingController.descriptionFocusNode, + onChanged: (_) => _updateServer() + ), ) ), SettingTile( @@ -131,28 +136,32 @@ class _HostingPageState extends RebootPageState { ), title: Text(translations.hostGameServerPasswordName), subtitle: Text(translations.hostGameServerDescriptionDescription), - content: Obx(() => TextFormBox( - placeholder: translations.hostGameServerPasswordName, - controller: _hostingController.password, - autovalidateMode: AutovalidateMode.always, - obscureText: !_hostingController.showPassword.value, - enableSuggestions: false, - autocorrect: false, - onChanged: (text) { - _showPasswordTrailing.value = text.isNotEmpty; - _updateServer(); - }, - suffix: Button( - onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value, - style: ButtonStyle( - shape: ButtonState.all(const CircleBorder()), - backgroundColor: ButtonState.all(Colors.transparent) - ), - child: Icon( - _hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled, - color: _showPasswordTrailing.value ? null : Colors.transparent - ), - ) + content: Obx(() => OverlayTarget( + key: hostInfoPasswordOverlayTargetKey, + child: TextFormBox( + placeholder: translations.hostGameServerPasswordName, + controller: _hostingController.password, + focusNode: _hostingController.passwordFocusNode, + autovalidateMode: AutovalidateMode.always, + obscureText: !_hostingController.showPassword.value, + enableSuggestions: false, + autocorrect: false, + onChanged: (text) { + _showPasswordTrailing.value = text.isNotEmpty; + _updateServer(); + }, + suffix: Button( + onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value, + style: ButtonStyle( + shape: ButtonState.all(const CircleBorder()), + backgroundColor: ButtonState.all(Colors.transparent) + ), + child: Icon( + _hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled, + color: _showPasswordTrailing.value ? null : Colors.transparent + ), + ) + ), )) ), SettingTile( @@ -183,105 +192,6 @@ class _HostingPageState extends RebootPageState { ] ); - SettingTile get _internalFiles => SettingTile( - icon: Icon( - FluentIcons.archive_settings_24_regular - ), - title: Text(translations.settingsServerName), - subtitle: Text(translations.settingsServerSubtitle), - children: [ - SettingTile( - icon: Icon( - FluentIcons.timer_24_regular - ), - title: Text(translations.settingsServerTypeName), - subtitle: Text(translations.settingsServerTypeDescription), - content: Obx(() => DropDownButton( - onOpen: () => inDialog = true, - onClose: () => inDialog = false, - leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), - items: { - false: translations.settingsServerTypeEmbeddedName, - true: translations.settingsServerTypeCustomName - }.entries.map((entry) => MenuFlyoutItem( - text: Text(entry.value), - onPressed: () { - final oldValue = _updateController.customGameServer.value; - if(oldValue == entry.key) { - return; - } - - _updateController.customGameServer.value = entry.key; - _updateController.infoBarEntry?.close(); - if(!entry.key) { - _updateController.updateReboot( - force: true - ); - } - } - )).toList() - )) - ), - Obx(() { - if(!_updateController.customGameServer.value) { - return const SizedBox.shrink(); - } - - return createFileSetting( - title: translations.settingsServerFileName, - description: translations.settingsServerFileDescription, - controller: _settingsController.gameServerDll - ); - }), - Obx(() { - if(_updateController.customGameServer.value) { - return const SizedBox.shrink(); - } - - return SettingTile( - icon: Icon( - FluentIcons.globe_24_regular - ), - title: Text(translations.settingsServerMirrorName), - subtitle: Text(translations.settingsServerMirrorDescription), - content: TextFormBox( - placeholder: translations.settingsServerMirrorPlaceholder, - controller: _updateController.url, - validator: checkUpdateUrl - ) - ); - }), - Obx(() { - if(_updateController.customGameServer.value) { - return const SizedBox.shrink(); - } - - return SettingTile( - icon: Icon( - FluentIcons.timer_24_regular - ), - title: Text(translations.settingsServerTimerName), - subtitle: Text(translations.settingsServerTimerSubtitle), - content: Obx(() => DropDownButton( - onOpen: () => inDialog = true, - onClose: () => inDialog = false, - leading: Text(_updateController.timer.value.text), - items: UpdateTimer.values.map((entry) => MenuFlyoutItem( - text: Text(entry.text), - onPressed: () { - _updateController.timer.value = entry; - _updateController.infoBarEntry?.close(); - _updateController.updateReboot( - force: true - ); - } - )).toList() - )) - ); - }), - ], - ); - SettingTile get _options => SettingTile( icon: Icon( FluentIcons.options_24_regular @@ -347,12 +257,121 @@ class _HostingPageState extends RebootPageState { ], ); + SettingTile get _internalFiles => SettingTile( + icon: Icon( + FluentIcons.archive_settings_24_regular + ), + title: Text(translations.settingsServerName), + subtitle: Text(translations.settingsServerSubtitle), + children: [ + SettingTile( + icon: Icon( + FluentIcons.timer_24_regular + ), + title: Text(translations.settingsServerTypeName), + subtitle: Text(translations.settingsServerTypeDescription), + content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), + items: { + false: translations.settingsServerTypeEmbeddedName, + true: translations.settingsServerTypeCustomName + }.entries.map((entry) => MenuFlyoutItem( + text: Text(entry.value), + onPressed: () { + final oldValue = _settingsController.customGameServer.value; + if(oldValue == entry.key) { + return; + } + + _settingsController.customGameServer.value = entry.key; + _settingsController.infoBarEntry?.close(); + if(!entry.key) { + _settingsController.updateReboot( + force: true + ); + } + } + )).toList() + )) + ), + Obx(() { + if(!_settingsController.customGameServer.value) { + return const SizedBox.shrink(); + } + + return createFileSetting( + title: translations.settingsServerFileName, + description: translations.settingsServerFileDescription, + controller: _settingsController.gameServerDll + ); + }), + Obx(() { + if(_settingsController.customGameServer.value) { + return const SizedBox.shrink(); + } + + return SettingTile( + icon: Icon( + FluentIcons.globe_24_regular + ), + title: Text(translations.settingsServerMirrorName), + subtitle: Text(translations.settingsServerMirrorDescription), + content: TextFormBox( + placeholder: translations.settingsServerMirrorPlaceholder, + controller: _settingsController.url, + validator: _checkUpdateUrl + ) + ); + }), + Obx(() { + if(_settingsController.customGameServer.value) { + return const SizedBox.shrink(); + } + + return SettingTile( + icon: Icon( + FluentIcons.timer_24_regular + ), + title: Text(translations.settingsServerTimerName), + subtitle: Text(translations.settingsServerTimerSubtitle), + content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text(_settingsController.timer.value.text), + items: UpdateTimer.values.map((entry) => MenuFlyoutItem( + text: Text(entry.text), + onPressed: () { + _settingsController.timer.value = entry; + _settingsController.infoBarEntry?.close(); + _settingsController.updateReboot( + force: true + ); + } + )).toList() + )) + ); + }), + ], + ); + + + String? _checkUpdateUrl(String? text) { + if (text == null || text.isEmpty) { + return translations.emptyURL; + } + + return null; + } + SettingTile get _share => SettingTile( icon: Icon( FluentIcons.link_24_regular ), title: Text(translations.hostShareName), subtitle: Text(translations.hostShareDescription), + overlayKey: hostShareOverlayTargetKey, children: [ SettingTile( icon: Icon( @@ -382,7 +401,7 @@ class _HostingPageState extends RebootPageState { final ip = await Ipify.ipv4(); entry.close(); FlutterClipboard.controlC(ip); - _showCopiedIp(); + _showCopiedIp(); }catch(error) { entry?.close(); _showCannotCopyIp(error); @@ -394,6 +413,18 @@ class _HostingPageState extends RebootPageState { ], ); + SettingTile get _resetDefaults => SettingTile( + icon: Icon( + FluentIcons.arrow_reset_24_regular + ), + title: Text(translations.hostResetName), + subtitle: Text(translations.hostResetDescription), + content: Button( + onPressed: () => showResetDialog(_hostingController.reset), + child: Text(translations.hostResetContent), + ) + ); + Future _updateServer() async { if(!_hostingController.published()) { return; @@ -409,29 +440,29 @@ class _HostingPageState extends RebootPageState { } } - void _showCopiedLink() => showInfoBar( + void _showCopiedLink() => showRebootInfoBar( translations.hostShareLinkMessageSuccess, severity: InfoBarSeverity.success ); - InfoBarEntry _showCopyingIp() => showInfoBar( + InfoBarEntry _showCopyingIp() => showRebootInfoBar( translations.hostShareIpMessageLoading, loading: true, duration: null ); - void _showCopiedIp() => showInfoBar( + void _showCopiedIp() => showRebootInfoBar( translations.hostShareIpMessageSuccess, severity: InfoBarSeverity.success ); - void _showCannotCopyIp(Object error) => showInfoBar( + void _showCannotCopyIp(Object error) => showRebootInfoBar( translations.hostShareIpMessageError(error.toString()), severity: InfoBarSeverity.error, duration: infoBarLongDuration ); - void _showCannotUpdateGameServer(Object error) => showInfoBar( + void _showCannotUpdateGameServer(Object error) => showRebootInfoBar( translations.cannotUpdateGameServer(error.toString()), severity: InfoBarSeverity.success, duration: infoBarLongDuration diff --git a/gui/lib/src/page/implementation/info_page.dart b/gui/lib/src/page/implementation/info_page.dart index 19d02b8..5608973 100644 --- a/gui/lib/src/page/implementation/info_page.dart +++ b/gui/lib/src/page/implementation/info_page.dart @@ -1,80 +1,14 @@ -import 'dart:collection'; -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; -import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:reboot_launcher/src/messenger/implementation/onboard.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; -import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -import 'package:path/path.dart' as path; -import 'package:reboot_launcher/src/widget/info_tile.dart'; +import 'package:reboot_launcher/src/widget/setting_tile.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class InfoPage extends RebootPage { - static late List _infoTiles; - static late List<_QuizEntry> _quizEntries; - - static Object? initInfoTiles() { - try { - final faqDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\faq"); - final infoTiles = SplayTreeMap(); - for(final entry in faqDirectory.listSync()) { - if(entry is File) { - final name = Uri.decodeQueryComponent(path.basename(entry.path)); - final splitter = name.indexOf("."); - if(splitter == -1) { - continue; - } - - final index = int.tryParse(name.substring(0, splitter)); - if(index == null) { - continue; - } - - final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2)); - infoTiles[index] = InfoTile( - title: Text(questionName), - content: Text(entry.readAsStringSync()) - ); - } - } - _infoTiles = infoTiles.values.toList(growable: false); - - final questionsDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\questions"); - final questions = SplayTreeMap(); - for(final entry in questionsDirectory.listSync()) { - if(entry is File) { - final name = Uri.decodeQueryComponent(path.basename(entry.path)); - final splitter = name.indexOf("."); - if(splitter == -1) { - continue; - } - - final index = int.tryParse(name.substring(0, splitter)); - if(index == null) { - continue; - } - - final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2)); - questions[index] = _QuizEntry( - question: questionName, - options: entry.readAsStringSync().split("\n") - ); - } - } - _quizEntries = questions.values.toList(growable: false); - - return null; - }catch(error) { - _infoTiles = []; - _quizEntries = []; - return error; - } - } - const InfoPage({Key? key}) : super(key: key); @override @@ -87,207 +21,59 @@ class InfoPage extends RebootPage { String get iconAsset => "assets/images/info.png"; @override - bool hasButton(String? pageName) => Get.find().firstRun.value && pageName != null; + bool hasButton(String? routeName) => false; @override RebootPageType get type => RebootPageType.info; } class _InfoPageState extends RebootPageState { - final SettingsController _settingsController = Get.find(); - late final Rxn _quizPage; + static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new"; + static const String _kDiscordInviteUrl = "https://discord.gg/reboot"; + + @override + List get settings => [ + _discord, + _tutorial, + _reportBug + ]; + + SettingTile get _reportBug => SettingTile( + icon: Icon( + FluentIcons.bug_24_regular + ), + title: Text(translations.settingsUtilsBugReportName), + subtitle: Text(translations.settingsUtilsBugReportSubtitle), + content: Button( + onPressed: () => launchUrlString(_kReportBugUrl), + child: Text(translations.settingsUtilsBugReportContent), + ) + ); + + SettingTile get _tutorial => SettingTile( + icon: Icon( + FluentIcons.chat_help_24_regular + ), + title: Text(translations.infoVideoName), + subtitle: Text(translations.infoVideoDescription), + content: Button( + onPressed: () => startOnboarding(), + child: Text(translations.infoVideoContent) + ) + ); + + SettingTile get _discord => SettingTile( + icon: Icon( + Icons.discord_outlined + ), + title: Text(translations.infoDiscordName), + subtitle: Text(translations.infoDiscordDescription), + content: Button( + onPressed: () => launchUrlString(_kDiscordInviteUrl), + child: Text(translations.infoDiscordContent) + ) + ); @override - void initState() { - _quizPage = Rxn(_settingsController.firstRun.value ? _QuizRoute( - entries: InfoPage._quizEntries, - onSuccess: () => _quizPage.value = null - ) : null); - super.initState(); - } - - @override - List get settings => InfoPage._infoTiles; - - @override - Widget? get button { - if(_quizPage.value == null) { - return null; - } - - return Obx(() { - final page = _quizPage.value; - if(page == null) { - return const SizedBox.shrink(); - } - - return SizedBox( - width: double.infinity, - height: 48, - child: Button( - onPressed: () => Navigator.of(context).push(PageRouteBuilder( - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - settings: RouteSettings( - name: translations.quiz - ), - pageBuilder: (context, incoming, outgoing) => page - )), - child: Text( - translations.startQuiz - ), - ) - ); - }); - } -} - -class _QuizRoute extends StatefulWidget { - final List<_QuizEntry> entries; - final void Function() onSuccess; - const _QuizRoute({ - required this.entries, - required this.onSuccess - }); - - @override - State<_QuizRoute> createState() => _QuizRouteState(); -} - -class _QuizRouteState extends State<_QuizRoute> with AutomaticKeepAliveClientMixin { - final SettingsController _settingsController = Get.find(); - late final List _selectedIndexes = List.generate(widget.entries.length, (_) => RxInt(-1)); - int _triesLeft = 3; - - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return Column( - children: [ - Expanded( - child: ListView( - children: widget.entries.indexed.expand((entry) { - final selectedIndex = _selectedIndexes[entry.$1]; - return [ - Text( - "${entry.$1 + 1}. ${entry.$2.question}", - style: TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.w600 - ), - ), - const SizedBox(height: 12.0), - ...entry.$2.options.indexed.map((value) => Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Obx(() => RadioButton( - checked: value.$1 == selectedIndex.value, - content: Text(value.$2, textAlign: TextAlign.center), - onChanged: (_) => selectedIndex.value = value.$1 - )), - )), - const SizedBox(height: 12.0) - ]; - }).toList() - ), - ), - const SizedBox( - height: 8.0, - ), - SizedBox( - width: double.infinity, - height: 48, - child: Obx(() { - var clickable = true; - for(final index in _selectedIndexes) { - if(index.value == -1) { - clickable = false; - break; - } - } - - return Button( - onPressed: clickable ? () async { - if(_triesLeft <= 0) { - return; - } - - var right = 0; - final total = widget.entries.length; - for(var i = 0; i < total; i++) { - final selectedIndex = _selectedIndexes[i].value; - final correctIndex = widget.entries[i].correctIndex; - if(selectedIndex == correctIndex) { - right++; - } - } - - if(right == total) { - widget.onSuccess(); - showInfoBar( - translations.quizSuccess, - severity: InfoBarSeverity.success - ); - _settingsController.firstRun.value = false; - Navigator.of(context).pop(); - pageIndex.value = RebootPageType.play.index; - return; - } - - switch(--_triesLeft) { - case 0: - showInfoBar( - translations.quizFailed( - right, - total, - translations.quizZeroTriesLeft - ), - severity: InfoBarSeverity.error - ); - await Future.delayed(const Duration(seconds: 1)); - exit(0); - case 1: - showInfoBar( - translations.quizFailed( - right, - total, - translations.quizOneTryLeft - ), - severity: InfoBarSeverity.error - ); - break; - case 2: - showInfoBar( - translations.quizFailed( - right, - total, - translations.quizTwoTriesLeft - ), - severity: InfoBarSeverity.error - ); - break; - } - } : null, - child: Text(translations.checkQuiz), - ); - }, - ), - ) - ], - ); - } -} - -class _QuizEntry { - final String question; - final List options; - late final int correctIndex; - - _QuizEntry({required this.question, required this.options}) { - final correct = options.first; - options.shuffle(); - correctIndex = options.indexOf(correct); - } + Widget? get button => null; } \ No newline at end of file diff --git a/gui/lib/src/page/implementation/play_page.dart b/gui/lib/src/page/implementation/play_page.dart index fc0760e..a42f2db 100644 --- a/gui/lib/src/page/implementation/play_page.dart +++ b/gui/lib/src/page/implementation/play_page.dart @@ -1,18 +1,19 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:get/get.dart'; -import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; +import 'package:reboot_launcher/src/messenger/implementation/onboard.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; -import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/file_setting_tile.dart'; import 'package:reboot_launcher/src/widget/game_start_button.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/version_selector_tile.dart'; +final GlobalKey gameVersionOverlayTargetKey = GlobalKey(); class PlayPage extends RebootPage { const PlayPage({Key? key}) : super(key: key); @@ -36,8 +37,49 @@ class PlayPage extends RebootPage { class _PlayPageState extends RebootPageState { final SettingsController _settingsController = Get.find(); final GameController _gameController = Get.find(); - final BackendController _backendController = Get.find(); - + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFirstLaunchInfo(), + Expanded( + child: super.build(context), + ) + ], + ); + } + + Widget _buildFirstLaunchInfo() => Obx(() { + if(!_settingsController.firstRun.value) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only( + bottom: 8.0 + ), + child: SizedBox( + width: double.infinity, + child: InfoBar( + title: Text(translations.welcomeTitle), + severity: InfoBarSeverity.warning, + isLong: true, + content: SizedBox( + width: double.infinity, + child: Text(translations.welcomeDescription) + ), + action: Button( + child: Text(translations.welcomeAction), + onPressed: () => startOnboarding(), + ), + onClose: () => _settingsController.firstRun.value = false + ), + ), + ); + }); + @override Widget? get button => LaunchButton( startLabel: translations.launchFortnite, @@ -47,25 +89,13 @@ class _PlayPageState extends RebootPageState { @override List get settings => [ - versionSelectSettingTile, + buildVersionSelector( + key: gameVersionOverlayTargetKey + ), _options, _internalFiles, - _multiplayer ]; - SettingTile get _multiplayer => SettingTile( - icon: Icon( - FluentIcons.people_24_regular - ), - title: Text(translations.playGameServerName), - subtitle: Text(translations.playGameServerDescription), - children: [ - _hostSettingTile, - _browseServerTile, - _matchmakerTile, - ], - ); - SettingTile get _internalFiles => SettingTile( icon: Icon( FluentIcons.archive_settings_24_regular @@ -95,8 +125,8 @@ class _PlayPageState extends RebootPageState { icon: Icon( FluentIcons.options_24_regular ), - title: Text(translations.settingsServerOptionsName), - subtitle: Text(translations.settingsServerOptionsSubtitle), + title: Text(translations.settingsClientOptionsName), + subtitle: Text(translations.settingsClientOptionsDescription), children: [ SettingTile( icon: Icon( @@ -111,34 +141,4 @@ class _PlayPageState extends RebootPageState { ) ] ); - - SettingTile get _matchmakerTile => SettingTile( - onPressed: () { - pageIndex.value = RebootPageType.backend.index; - WidgetsBinding.instance.addPostFrameCallback((_) => _backendController.gameServerAddressFocusNode.requestFocus()); - }, - icon: Icon( - FluentIcons.globe_24_regular - ), - title: Text(translations.playGameServerCustomName), - subtitle: Text(translations.playGameServerCustomDescription), - ); - - SettingTile get _browseServerTile => SettingTile( - onPressed: () => pageIndex.value = RebootPageType.browser.index, - icon: Icon( - FluentIcons.search_24_regular - ), - title: Text(translations.playGameServerBrowserName), - subtitle: Text(translations.playGameServerBrowserDescription) - ); - - SettingTile get _hostSettingTile => SettingTile( - onPressed: () => pageIndex.value = RebootPageType.host.index, - icon: Icon( - FluentIcons.desktop_24_regular - ), - title: Text(translations.playGameServerHostName), - subtitle: Text(translations.playGameServerHostDescription), - ); } \ No newline at end of file diff --git a/gui/lib/src/page/implementation/server_browser_page.dart b/gui/lib/src/page/implementation/server_browser_page.dart deleted file mode 100644 index f4349e5..0000000 --- a/gui/lib/src/page/implementation/server_browser_page.dart +++ /dev/null @@ -1,237 +0,0 @@ - -import 'dart:async'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/backend_controller.dart'; -import 'package:reboot_launcher/src/controller/hosting_controller.dart'; -import 'package:reboot_launcher/src/dialog/implementation/server.dart'; -import 'package:reboot_launcher/src/page/abstract/page.dart'; -import 'package:reboot_launcher/src/page/abstract/page_type.dart'; -import 'package:reboot_launcher/src/util/translations.dart'; -import 'package:reboot_launcher/src/widget/setting_tile.dart'; - -class BrowsePage extends RebootPage { - const BrowsePage({Key? key}) : super(key: key); - - @override - String get name => translations.browserName; - - @override - RebootPageType get type => RebootPageType.browser; - - @override - String get iconAsset => "assets/images/server_browser.png"; - - @override - bool hasButton(String? pageName) => false; - - @override - RebootPageState createState() => _BrowsePageState(); -} - -class _BrowsePageState extends RebootPageState { - final HostingController _hostingController = Get.find(); - final BackendController _backendController = Get.find(); - final TextEditingController _filterController = TextEditingController(); - final StreamController _filterControllerStream = StreamController.broadcast(); - - @override - Widget build(BuildContext context) { - super.build(context); - return Obx(() { - var data = _hostingController.servers.value - ?.where((entry) => (kDebugMode || entry["id"] != _hostingController.uuid) && entry["discoverable"]) - .toSet(); - if(data == null || data.isEmpty == true) { - return _noServers; - } - - return _buildPageBody(data); - }); - } - - Widget get _noServers => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - translations.noServersAvailableTitle, - style: FluentTheme.of(context).typography.titleLarge, - ), - Text( - translations.noServersAvailableSubtitle, - style: FluentTheme.of(context).typography.body - ), - ], - ); - - Widget _buildPageBody(Set> data) => StreamBuilder( - stream: _filterControllerStream.stream, - builder: (context, filterSnapshot) { - final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet(); - return Column( - children: [ - _searchBar, - const SizedBox( - height: 16, - ), - Expanded( - child: items.isEmpty ? _noServersByQuery : _buildPopulatedListBody(items) - ), - ], - ); - } - ); - - Widget _buildPopulatedListBody(Set> items) => ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - var entry = items.elementAt(index ~/ 2); - var hasPassword = entry["password"] != null; - return SettingTile( - icon: Icon( - hasPassword ? FluentIcons.lock : FluentIcons.globe - ), - title: Text("${_formatName(entry)} • ${entry["author"]}"), - subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"), - content: Button( - onPressed: () => _backendController.joinServer(_hostingController.uuid, entry), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp), - ], - ), - ) - ); - } - ); - - Widget get _noServersByQuery => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - translations.noServersAvailableByQueryTitle, - style: FluentTheme.of(context).typography.titleLarge, - ), - Text( - translations.noServersAvailableByQuerySubtitle, - style: FluentTheme.of(context).typography.body - ), - ], - ); - - bool _isValidItem(Map entry, String? filter) => - filter == null || filter.isEmpty || _filterServer(entry, filter); - - bool _filterServer(Map element, String filter) { - String? id = element["id"]; - if(id?.toLowerCase().contains(filter.toLowerCase()) == true) { - return true; - } - - var uri = Uri.tryParse(filter); - if(uri != null - && uri.host.isNotEmpty - && id?.toLowerCase().contains(uri.host.toLowerCase()) == true) { - return true; - } - - String? name = element["name"]; - if(name?.toLowerCase().contains(filter) == true) { - return true; - } - - String? author = element["author"]; - if(author?.toLowerCase().contains(filter) == true) { - return true; - } - - String? description = element["description"]; - if(description?.toLowerCase().contains(filter) == true) { - return true; - } - - return false; - } - - Widget get _searchBar => Align( - alignment: Alignment.centerLeft, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 350 - ), - child: TextBox( - placeholder: translations.findServer, - controller: _filterController, - autofocus: true, - onChanged: (value) => _filterControllerStream.add(value), - suffix: _searchBarIcon, - ), - ), - ); - - Widget get _searchBarIcon => Button( - onPressed: _filterController.text.isEmpty ? null : () { - _filterController.clear(); - _filterControllerStream.add(""); - }, - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.transparent), - shape: ButtonState.all(Border()) - ), - child: _searchBarIconData - ); - - Widget get _searchBarIconData { - var color = FluentTheme.of(context).resources.textFillColorPrimary; - if (_filterController.text.isNotEmpty) { - return Icon( - FluentIcons.clear, - size: 8.0, - color: color - ); - } - - return Transform.flip( - flipX: true, - child: Icon( - FluentIcons.search, - size: 12.0, - color: color - ), - ); - } - - String _formatName(Map entry) { - String result = entry['name']; - return result.isEmpty ? translations.defaultServerName : result; - } - - String _formatDescription(Map entry) { - String result = entry['description']; - return result.isEmpty ? translations.defaultServerDescription : result; - } - - String _formatVersion(Map entry) { - var version = entry['version']; - var versionSplit = version.indexOf("-"); - var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version; - String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion; - if(result.toLowerCase().startsWith("fortnite ")) { - result = result.substring(0, 10); - } - - return "Fortnite $result"; - } - - @override - Widget? get button => null; - - @override - List get settings => []; -} diff --git a/gui/lib/src/page/implementation/settings_page.dart b/gui/lib/src/page/implementation/settings_page.dart index 7dd05e8..80eefd6 100644 --- a/gui/lib/src/page/implementation/settings_page.dart +++ b/gui/lib/src/page/implementation/settings_page.dart @@ -5,8 +5,8 @@ import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; -import 'package:reboot_launcher/src/dialog/implementation/data.dart'; +import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/util/translations.dart'; @@ -40,52 +40,67 @@ class _SettingsPageState extends RebootPageState { @override List get settings => [ - SettingTile( - icon: Icon( - FluentIcons.local_language_24_regular - ), - title: Text(translations.settingsUtilsLanguageName), - subtitle: Text(translations.settingsUtilsLanguageDescription), - content: Obx(() => DropDownButton( - onOpen: () => inDialog = true, - onClose: () => inDialog = false, - leading: Text(_getLocaleName(_settingsController.language.value)), - items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem( - text: Text(_getLocaleName(locale.languageCode)), - onPressed: () => _settingsController.language.value = locale.languageCode - )).toList() - )) - ), - SettingTile( - icon: Icon( - FluentIcons.dark_theme_24_regular - ), - title: Text(translations.settingsUtilsThemeName), - subtitle: Text(translations.settingsUtilsThemeDescription), - content: Obx(() => DropDownButton( - onOpen: () => inDialog = true, - onClose: () => inDialog = false, - leading: Text(_settingsController.themeMode.value.title), - items: ThemeMode.values.map((themeMode) => MenuFlyoutItem( - text: Text(themeMode.title), - onPressed: () => _settingsController.themeMode.value = themeMode - )).toList() - )) - ), - SettingTile( - icon: Icon( - FluentIcons.arrow_reset_24_regular - ), - title: Text(translations.settingsUtilsResetDefaultsName), - subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle), - content: Button( - onPressed: () => showResetDialog(_settingsController.reset), - child: Text(translations.settingsUtilsResetDefaultsContent), - ) - ), + _language, + _theme, + _resetDefaults, _installationDirectory ]; + SettingTile get _language => SettingTile( + icon: Icon( + FluentIcons.local_language_24_regular + ), + title: Text(translations.settingsUtilsLanguageName), + subtitle: Text(translations.settingsUtilsLanguageDescription), + content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text(_getLocaleName(_settingsController.language.value)), + items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem( + text: Text(_getLocaleName(locale.languageCode)), + onPressed: () => _settingsController.language.value = locale.languageCode + )).toList() + )) + ); + + String _getLocaleName(String locale) { + var result = LocaleNames.of(context)!.nameOf(locale); + if(result != null) { + return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}"; + } + + return locale; + } + + SettingTile get _theme => SettingTile( + icon: Icon( + FluentIcons.dark_theme_24_regular + ), + title: Text(translations.settingsUtilsThemeName), + subtitle: Text(translations.settingsUtilsThemeDescription), + content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text(_settingsController.themeMode.value.title), + items: ThemeMode.values.map((themeMode) => MenuFlyoutItem( + text: Text(themeMode.title), + onPressed: () => _settingsController.themeMode.value = themeMode + )).toList() + )) + ); + + SettingTile get _resetDefaults => SettingTile( + icon: Icon( + FluentIcons.arrow_reset_24_regular + ), + title: Text(translations.settingsUtilsResetDefaultsName), + subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle), + content: Button( + onPressed: () => showResetDialog(_settingsController.reset), + child: Text(translations.settingsUtilsResetDefaultsContent), + ) + ); + SettingTile get _installationDirectory => SettingTile( icon: Icon( FluentIcons.folder_24_regular @@ -97,15 +112,6 @@ class _SettingsPageState extends RebootPageState { child: Text(translations.settingsUtilsInstallationDirectoryContent), ) ); - - String _getLocaleName(String locale) { - var result = LocaleNames.of(context)!.nameOf(locale); - if(result != null) { - return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}"; - } - - return locale; - } } extension _ThemeModeExtension on ThemeMode { diff --git a/gui/lib/src/page/pages.dart b/gui/lib/src/page/pages.dart index e4c07a0..802376e 100644 --- a/gui/lib/src/page/pages.dart +++ b/gui/lib/src/page/pages.dart @@ -3,14 +3,14 @@ import 'dart:collection'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; -import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/implementation/backend_page.dart'; +import 'package:reboot_launcher/src/page/implementation/browser_page.dart'; +import 'package:reboot_launcher/src/page/implementation/host_page.dart'; import 'package:reboot_launcher/src/page/implementation/info_page.dart'; import 'package:reboot_launcher/src/page/implementation/play_page.dart'; -import 'package:reboot_launcher/src/page/implementation/server_browser_page.dart'; -import 'package:reboot_launcher/src/page/implementation/server_host_page.dart'; import 'package:reboot_launcher/src/page/implementation/settings_page.dart'; import 'package:reboot_launcher/src/widget/info_bar_area.dart'; @@ -26,15 +26,15 @@ final List pages = [ const SettingsPage() ]; -final RxInt pageIndex = _initialPageIndex; -RxInt get _initialPageIndex { - final settingsController = Get.find(); - return RxInt(settingsController.firstRun.value ? RebootPageType.info.index : RebootPageType.play.index); -} +final List> _flyoutPageControllers = List.generate(pages.length, (_) => GlobalKey()); + +final RxInt pageIndex = RxInt(RebootPageType.play.index); final HashMap _pageKeys = HashMap(); -final GlobalKey appKey = GlobalKey(); +final GlobalKey appNavigatorKey = GlobalKey(); + +final GlobalKey appOverlayKey = GlobalKey(); final GlobalKey infoBarAreaKey = GlobalKey(); @@ -79,4 +79,8 @@ void addSubPageToStack(String pageName) { appStack.add(identifier); _pagesStack[index]!.add(identifier); pagesController.add(null); -} \ No newline at end of file +} + +GlobalKey getOverlayTargetKeyByPage(int pageIndex) => _flyoutPageControllers[pageIndex]; + +GlobalKey get pageOverlayTargetKey => _flyoutPageControllers[pageIndex.value]; diff --git a/gui/lib/src/util/checks.dart b/gui/lib/src/util/checks.dart deleted file mode 100644 index 84ec47f..0000000 --- a/gui/lib/src/util/checks.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:io'; - -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/util/translations.dart'; - -String? checkVersion(String? text, List versions) { - if (text == null || text.isEmpty) { - return translations.emptyVersionName; - } - - if (versions.any((element) => element.name == text)) { - return translations.versionAlreadyExists; - } - - return null; -} - -String? checkChangeVersion(String? text) { - if (text == null || text.isEmpty) { - return translations.emptyVersionName; - } - - return null; -} - -String? checkGameFolder(text) { - if (text == null || text.isEmpty) { - return translations.emptyGamePath; - } - - var directory = Directory(text); - if (!directory.existsSync()) { - return translations.directoryDoesNotExist; - } - - if (FortniteVersionExtension.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) { - return translations.missingShippingExe; - } - - return null; -} - -String? checkDownloadDestination(text) { - if (text == null || text.isEmpty) { - return translations.invalidDownloadPath; - } - - return null; -} - -String? checkDll(String? text) { - if (text == null || text.isEmpty) { - return translations.invalidDllPath; - } - - if (!File(text).existsSync()) { - return translations.dllDoesNotExist; - } - - if (!text.endsWith(".dll")) { - return translations.invalidDllExtension; - } - - return null; -} - -String? checkMatchmaking(String? text) { - if (text == null || text.isEmpty) { - return translations.emptyHostname; - } - - var ipParts = text.split(":"); - if(ipParts.length > 2){ - return translations.hostnameFormat; - } - - return null; -} - -String? checkUpdateUrl(String? text) { - if (text == null || text.isEmpty) { - return translations.emptyURL; - } - - return null; -} \ No newline at end of file diff --git a/gui/lib/src/util/dll.dart b/gui/lib/src/util/dll.dart deleted file mode 100644 index 06cdf70..0000000 --- a/gui/lib/src/util/dll.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:path/path.dart' as path; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/controller/update_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; -import 'package:reboot_launcher/src/util/translations.dart'; - -final UpdateController _updateController = Get.find(); -final Map> _operations = {}; - -Future downloadCriticalDllInteractive(String filePath, {bool silent = false}) { - final old = _operations[filePath]; - if(old != null) { - return old; - } - - final newRun = _downloadCriticalDllInteractive(filePath, silent); - _operations[filePath] = newRun; - return newRun; -} - -Future _downloadCriticalDllInteractive(String filePath, bool silent) async { - final fileName = path.basename(filePath).toLowerCase(); - InfoBarEntry? entry; - try { - if (fileName == "reboot.dll") { - await _updateController.updateReboot( - silent: silent - ); - return; - } - - if(File(filePath).existsSync()) { - return; - } - - final fileNameWithoutExtension = path.basenameWithoutExtension(filePath); - if(!silent) { - entry = showInfoBar( - translations.downloadingDll(fileNameWithoutExtension), - loading: true, - duration: null - ); - } - await downloadCriticalDll(fileName, filePath); - entry?.close(); - if(!silent) { - entry = await showInfoBar( - translations.downloadDllSuccess(fileNameWithoutExtension), - severity: InfoBarSeverity.success, - duration: infoBarShortDuration - ); - } - }catch(message) { - if(!silent) { - entry?.close(); - var error = message.toString(); - error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; - error = error.toLowerCase(); - final completer = Completer(); - await showInfoBar( - translations.downloadDllError(fileName, error.toString()), - duration: infoBarLongDuration, - severity: InfoBarSeverity.error, - onDismissed: () => completer.complete(null), - action: Button( - onPressed: () async { - await downloadCriticalDllInteractive(filePath); - completer.complete(null); - }, - child: Text(translations.downloadDllRetry), - ) - ); - await completer.future; - } - }finally { - _operations.remove(fileName); - } -} - -extension InjectableDllExtension on InjectableDll { - String get path { - final SettingsController settingsController = Get.find(); - switch(this){ - case InjectableDll.reboot: - if(_updateController.customGameServer.value) { - final file = File(settingsController.gameServerDll.text); - if(file.existsSync()) { - return file.path; - } - } - - return rebootDllFile.path; - case InjectableDll.console: - return settingsController.unrealEngineConsoleDll.text; - case InjectableDll.cobalt: - return settingsController.backendDll.text; - case InjectableDll.memory: - return settingsController.memoryLeakDll.text; - } - } -} diff --git a/gui/lib/src/util/matchmaker.dart b/gui/lib/src/util/matchmaker.dart index 68353ca..872efa4 100644 --- a/gui/lib/src/util/matchmaker.dart +++ b/gui/lib/src/util/matchmaker.dart @@ -3,45 +3,52 @@ import 'dart:io'; import 'package:reboot_common/common.dart'; -const Duration _timeout = Duration(seconds: 2); - -Future _pingGameServer(String hostname, int port) async { - RawDatagramSocket? socket; - try { - socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); - final dataToSend = utf8.encode(DateTime.now().toIso8601String()); - socket.send(dataToSend, InternetAddress(hostname), port); - await for (var event in socket) { - switch(event) { - case RawSocketEvent.read: - case RawSocketEvent.write: - return true; - case RawSocketEvent.readClosed: - case RawSocketEvent.closed: - return false; - } - } - - return false; - }finally { - socket?.close(); - } -} - -Future get _timeoutFuture => Future.delayed(_timeout).then((value) => false); +const Duration _timeout = Duration(seconds: 5); Future pingGameServer(String address, {Duration? timeout}) async { - var start = DateTime.now(); - var firstTime = true; - while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) { - var split = address.split(":"); - var hostname = split[0]; - if(isLocalHost(hostname)) { - hostname = "127.0.0.1"; - } + Future ping(String hostname, int port) async { + log("[MATCHMAKER] Pinging $hostname:$port"); + RawDatagramSocket? socket; + try { + socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); + await for (final event in socket) { + log("[MATCHMAKER] Event: $event"); + switch(event) { + case RawSocketEvent.read: + log("[MATCHMAKER] Success"); + return true; + case RawSocketEvent.write: + log("[MATCHMAKER] Sending data"); + final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA=="); + socket.send(dataToSend, InternetAddress(hostname), port); + case RawSocketEvent.readClosed: + case RawSocketEvent.closed: + return false; + } + } - var port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort); - var result = await Future.any([_timeoutFuture, _pingGameServer(hostname, port)]); + return false; + }catch(error) { + log("[MATCHMAKER] Error: $error"); + return false; + }finally { + socket?.close(); + } + } + + + final start = DateTime.now(); + var firstTime = true; + final split = address.split(":"); + var hostname = split[0]; + if(isLocalHost(hostname)) { + hostname = "127.0.0.1"; + } + + final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort); + while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) { + final result = await ping(hostname, port) + .timeout(_timeout, onTimeout: () => false); if(result) { return true; } diff --git a/gui/lib/src/util/os.dart b/gui/lib/src/util/os.dart index a220549..470de94 100644 --- a/gui/lib/src/util/os.dart +++ b/gui/lib/src/util/os.dart @@ -3,10 +3,10 @@ import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/scheduler.dart'; import 'package:win32/win32.dart'; -import 'package:file_picker/file_picker.dart'; final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))'); diff --git a/gui/lib/src/widget/add_local_version.dart b/gui/lib/src/widget/add_local_version.dart deleted file mode 100644 index 42d8354..0000000 --- a/gui/lib/src/widget/add_local_version.dart +++ /dev/null @@ -1,103 +0,0 @@ -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/util/translations.dart'; -import 'package:reboot_launcher/src/widget/file_selector.dart'; -import 'package:reboot_launcher/src/widget/version_name_input.dart'; - -class AddLocalVersion extends StatefulWidget { - const AddLocalVersion({Key? key}) - : super(key: key); - - @override - State createState() => _AddLocalVersionState(); -} - -class _AddLocalVersionState extends State { - final GameController _gameController = Get.find(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _gamePathController = TextEditingController(); - - @override - void initState() { - _gamePathController.addListener(() async { - var file = Directory(_gamePathController.text); - if(await file.exists()) { - if(_nameController.text.isEmpty) { - var text = path.basename(_gamePathController.text); - _nameController.text = text; - _nameController.selection = TextSelection.collapsed(offset: text.length); - } - } - }); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return FormDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: double.infinity, - child: InfoBar( - title: Text(translations.localBuildsWarning), - severity: InfoBarSeverity.info - ), - ), - - const SizedBox( - height: 16.0 - ), - - VersionNameInput( - controller: _nameController - ), - - const SizedBox( - height: 16.0 - ), - - FileSelector( - label: translations.gameFolderTitle, - placeholder: translations.gameFolderPlaceholder, - windowTitle: translations.gameFolderPlaceWindowTitle, - controller: _gamePathController, - validator: checkGameFolder, - folder: true - ), - - const SizedBox( - height: 16.0 - ) - ], - ), - buttons: [ - DialogButton( - type: ButtonType.secondary - ), - - DialogButton( - text: translations.saveLocalVersion, - type: ButtonType.primary, - onTap: () { - Navigator.of(context).pop(); - WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion( - name: _nameController.text, - location: Directory(_gamePathController.text) - ))); - }, - ) - ] - ); - } -} diff --git a/gui/lib/src/widget/add_server_version.dart b/gui/lib/src/widget/add_server_version.dart deleted file mode 100644 index d79edc6..0000000 --- a/gui/lib/src/widget/add_server_version.dart +++ /dev/null @@ -1,347 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.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/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/util/translations.dart'; -import 'package:reboot_launcher/src/widget/file_selector.dart'; -import 'package:reboot_launcher/src/widget/version_name_input.dart'; -import 'package:universal_disk_space/universal_disk_space.dart'; -import 'package:windows_taskbar/windows_taskbar.dart'; - -class AddServerVersion extends StatefulWidget { - const AddServerVersion({Key? key}) : super(key: key); - - @override - State createState() => _AddServerVersionState(); -} - -class _AddServerVersionState extends State { - final GameController _gameController = Get.find(); - final BuildController _buildController = Get.find(); - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _pathController = TextEditingController(); - final Rx _status = Rx(DownloadStatus.form); - final GlobalKey _formKey = GlobalKey(); - final RxnInt _timeLeft = RxnInt(); - final Rxn _progress = Rxn(); - - late DiskSpace _diskSpace; - late Future _fetchFuture; - late Future _diskFuture; - - Isolate? _isolate; - SendPort? _downloadPort; - Object? _error; - StackTrace? _stackTrace; - - @override - void initState() { - _fetchFuture = _buildController.builds != null - ? Future.value(true) - : compute(fetchBuilds, null) - .then((value) => _buildController.builds = value); - _diskSpace = DiskSpace(); - _diskFuture = _diskSpace.scan() - .then((_) => _updateFormDefaults()); - super.initState(); - } - - @override - void dispose() { - _pathController.dispose(); - _nameController.dispose(); - _cancelDownload(); - super.dispose(); - } - - void _cancelDownload() { - Process.run('${assetsDirectory.path}\\build\\stop.bat', []); - _downloadPort?.send(kStopBuildDownloadSignal); - _isolate?.kill(priority: Isolate.immediate); - } - - @override - Widget build(BuildContext context) => Form( - key: _formKey, - child: Obx(() { - switch(_status.value){ - case DownloadStatus.form: - return FutureBuilder( - future: Future.wait([_fetchFuture, _diskFuture]), - builder: (context, snapshot) { - if (snapshot.hasError) { - WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace)); - } - - if (!snapshot.hasData) { - return ProgressDialog( - text: translations.fetchingBuilds, - onStop: () => Navigator.of(context).pop() - ); - } - - return FormDialog( - content: _formBody, - buttons: _formButtons - ); - } - ); - case DownloadStatus.downloading: - case DownloadStatus.extracting: - return GenericDialog( - header: _progressBody, - buttons: _stopButton - ); - case DownloadStatus.error: - return ErrorDialog( - exception: _error ?? Exception(translations.unknownError), - stackTrace: _stackTrace, - errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString()) - ); - case DownloadStatus.done: - return InfoDialog( - text: translations.downloadedVersion - ); - } - }) - ); - - List get _formButtons => [ - DialogButton(type: ButtonType.secondary), - DialogButton( - text: translations.download, - type: ButtonType.primary, - onTap: () => _startDownload(context), - ) - ]; - - void _startDownload(BuildContext context) async { - try { - final build = _buildController.selectedBuild; - if(build == null){ - return; - } - - _status.value = DownloadStatus.downloading; - final communicationPort = ReceivePort(); - communicationPort.listen((message) { - if(message is FortniteBuildDownloadProgress) { - _onProgress(message.progress, message.minutesLeft, message.extracting); - }else if(message is SendPort) { - _downloadPort = message; - }else { - _onDownloadError(message, null); - } - }); - final options = FortniteBuildDownloadOptions( - build, - Directory(_pathController.text), - communicationPort.sendPort - ); - final errorPort = ReceivePort(); - errorPort.listen((message) => _onDownloadError(message, null)); - _isolate = await Isolate.spawn( - downloadArchiveBuild, - options, - onError: errorPort.sendPort, - errorsAreFatal: true - ); - } catch (exception, stackTrace) { - _onDownloadError(exception, stackTrace); - } - } - - Future _onDownloadComplete() async { - if (!mounted) { - return; - } - - _status.value = DownloadStatus.done; - WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); - WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion( - name: _nameController.text, - location: Directory(_pathController.text) - ))); - } - - void _onDownloadError(Object? error, StackTrace? stackTrace) { - _cancelDownload(); - if (!mounted) { - return; - } - - _status.value = DownloadStatus.error; - WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); - _error = error; - _stackTrace = stackTrace; - } - - void _onProgress(double progress, int? timeLeft, bool extracting) { - if (!mounted) { - return; - } - - if(progress >= 100 && extracting) { - _onDownloadComplete(); - return; - } - - _status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading; - if(progress >= 0) { - WindowsTaskbar.setProgress(progress.round(), 100); - } - - _timeLeft.value = timeLeft; - _progress.value = progress; - } - - Widget get _progressBody { - final timeLeft = _timeLeft.value; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - _status.value == DownloadStatus.downloading ? translations.downloading : translations.extracting, - style: FluentTheme.maybeOf(context)?.typography.body, - textAlign: TextAlign.start, - ), - ), - - const SizedBox( - height: 8.0, - ), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - translations.buildProgress((_progress.value ?? 0).round()), - style: FluentTheme.maybeOf(context)?.typography.body, - ), - - if(timeLeft != null) - Text( - translations.timeLeft(timeLeft), - style: FluentTheme.maybeOf(context)?.typography.body, - ) - ], - ), - - const SizedBox( - height: 8.0, - ), - - SizedBox( - width: double.infinity, - child: ProgressBar(value: (_progress.value ?? 0).toDouble()) - ), - - const SizedBox( - height: 8.0, - ) - ], - ); - } - - Widget get _formBody => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSelector(), - - const SizedBox( - height: 16.0 - ), - - VersionNameInput( - controller: _nameController - ), - - const SizedBox( - height: 16.0 - ), - - FileSelector( - label: translations.buildInstallationDirectory, - placeholder: translations.buildInstallationDirectoryPlaceholder, - windowTitle: translations.buildInstallationDirectoryWindowTitle, - controller: _pathController, - validator: checkDownloadDestination, - folder: true - ), - - const SizedBox( - height: 16.0 - ) - ], - ); - - Widget _buildSelector() => InfoLabel( - label: translations.build, - child: Obx(() => ComboBox( - placeholder: Text(translations.selectBuild), - isExpanded: true, - items: _builds, - value: _buildController.selectedBuild, - onChanged: (value) { - if(value == null){ - return; - } - - _buildController.selectedBuild = value; - _updateFormDefaults(); - } - )) - ); - - List> get _builds => _buildController.builds! - .map((element) => _buildItem(element)) - .toList(); - - ComboBoxItem _buildItem(FortniteBuild element) => ComboBoxItem( - value: element, - child: Text(element.version.toString()) - ); - - List get _stopButton => [ - DialogButton( - text: "Stop", - type: ButtonType.only - ) - ]; - - Future _updateFormDefaults() async { - if(_diskSpace.disks.isEmpty){ - return; - } - - await _fetchFuture; - final bestDisk = _diskSpace.disks - .reduce((first, second) => first.availableSpace > second.availableSpace ? first : second); - final build = _buildController.selectedBuild; - if(build == null){ - return; - } - - final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}"; - _pathController.text = pathText; - _pathController.selection = TextSelection.collapsed(offset: pathText.length); - final buildName = build.version.toString(); - _nameController.text = buildName; - _nameController.selection = TextSelection.collapsed(offset: buildName.length); - _formKey.currentState?.validate(); - } -} - -enum DownloadStatus { form, downloading, extracting, error, done } diff --git a/gui/lib/src/widget/file_setting_tile.dart b/gui/lib/src/widget/file_setting_tile.dart index 65efc04..c2cf55b 100644 --- a/gui/lib/src/widget/file_setting_tile.dart +++ b/gui/lib/src/widget/file_setting_tile.dart @@ -1,6 +1,7 @@ +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; -import 'package:reboot_launcher/src/util/checks.dart'; import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/file_selector.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart'; @@ -15,8 +16,26 @@ SettingTile createFileSetting({required String title, required String descriptio placeholder: translations.selectPathPlaceholder, windowTitle: translations.selectPathWindowTitle, controller: controller, - validator: checkDll, + validator: _checkDll, extension: "dll", - folder: false + folder: false, + validatorMode: AutovalidateMode.always ) -); \ No newline at end of file +); + +String? _checkDll(String? text) { + if (text == null || text.isEmpty) { + return translations.invalidDllPath; + } + + final file = File(text); + if (!file.existsSync()) { + return translations.dllDoesNotExist; + } + + if (!text.endsWith(".dll")) { + return translations.invalidDllExtension; + } + + return null; +} \ No newline at end of file diff --git a/gui/lib/src/widget/game_start_button.dart b/gui/lib/src/widget/game_start_button.dart index 27dedcb..8ff33fd 100644 --- a/gui/lib/src/widget/game_start_button.dart +++ b/gui/lib/src/widget/game_start_button.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:async/async.dart'; import 'package:dart_ipify/dart_ipify.dart'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:path/path.dart'; @@ -13,17 +12,16 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; -import 'package:reboot_launcher/src/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/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/pages.dart'; -import 'package:reboot_launcher/src/util/dll.dart'; -import 'package:reboot_launcher/src/util/log.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class LaunchButton extends StatefulWidget { final bool host; @@ -43,6 +41,7 @@ class _LaunchButtonState extends State { final HostingController _hostingController = Get.find(); final BackendController _backendController = Get.find(); final SettingsController _settingsController = Get.find(); + InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameServerInfoBar; CancelableOperation? _operation; @@ -95,6 +94,10 @@ class _LaunchButtonState extends State { log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); for (final injectable in InjectableDll.values) { if(await _getDllFileOrStop(injectable, host) == null) { + _onStop( + reason: _StopReason.missingCustomDllError, + error: injectable.name, + ); return; } } @@ -124,12 +127,16 @@ class _LaunchButtonState extends State { log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)"); final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false); log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance"); - await _startGameProcesses(version, host, serverType, linkedHostingInstance); - if(!host) { - _showLaunchingGameClientWidget(); + final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance); + final started = host ? _hostingController.started() : _gameController.started(); + if(!started) { + result?.kill(); + return; } - if(linkedHostingInstance != null || host){ + if(!host) { + _showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null); + }else { _showLaunchingGameServerWidget(); } } catch (exception, stackTrace) { @@ -148,7 +155,7 @@ class _LaunchButtonState extends State { return null; } - if(_backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) { + if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) { log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server"); return null; } @@ -158,7 +165,7 @@ class _LaunchButtonState extends State { return null; } - final response = forceLinkedHosting || await _askForAutomaticGameServer(); + final response = forceLinkedHosting || await _askForAutomaticGameServer(host); if(!response) { log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server"); return null; @@ -172,19 +179,24 @@ class _LaunchButtonState extends State { return instance; } - Future _askForAutomaticGameServer() async { - final result = await showAppDialog( + Future _askForAutomaticGameServer(bool host) async { + if (host ? !_hostingController.started() : !_gameController.started()) { + log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance"); + _onStop(reason: _StopReason.normal); + return false; + } + + final result = await showRebootDialog( builder: (context) => InfoDialog( - text: "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. " - "If you don't want to join another player's server, you should start a game server. This is necessary to be able to play: for more information check the Info tab in the launcher.", + text: translations.automaticGameServerDialogContent, buttons: [ DialogButton( type: ButtonType.secondary, - text: "Ignore" + text: translations.automaticGameServerDialogIgnore ), DialogButton( type: ButtonType.primary, - text: "Start server", + text: translations.automaticGameServerDialogStart, onTap: () => Navigator.of(context).pop(true), ), ], @@ -214,7 +226,7 @@ class _LaunchButtonState extends State { log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}"); final instance = GameInstance( - versionName: version.name, + versionName: version.content.toString(), gamePid: gameProcess, launcherPid: launcherProcess, eacPid: eacProcess, @@ -247,7 +259,10 @@ class _LaunchButtonState extends State { executable: executable, args: gameArgs, useTempBatch: false, - name: "${version.name}-${host ? 'HOST' : 'GAME'}" + name: "${version.content}-${host ? 'HOST' : 'GAME'}", + environment: { + "OPENSSL_ia32cap": "~0x20000000" + } ); void onGameOutput(String line, bool error) { log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line"); @@ -267,12 +282,7 @@ class _LaunchButtonState extends State { gameProcess.stdError.listen((line) => onGameOutput(line, true)); gameProcess.exitCode.then((_) async { final instance = host ? _hostingController.instance.value : _gameController.instance.value; - if(instance == null) { - log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running"); - return; - } - - log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance.launched}): stop signal"); + log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal"); _onStop( reason: _StopReason.exitCode, host: host @@ -289,7 +299,10 @@ class _LaunchButtonState extends State { final process = await startProcess( executable: file, useTempBatch: false, - name: "${version.name}-${basenameWithoutExtension(file.path)}" + name: "${version.content}-${basenameWithoutExtension(file.path)}", + environment: { + "OPENSSL_ia32cap": "~0x20000000" + } ); final pid = process.pid; suspend(pid); @@ -304,7 +317,7 @@ class _LaunchButtonState extends State { try { final windowManager = VirtualDesktopManager.getInstance(); _virtualDesktop = windowManager.createDesktop(); - windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)"); + windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)"); var success = false; try { success = await windowManager.moveWindowToDesktop( @@ -391,7 +404,7 @@ class _LaunchButtonState extends State { void _onGameClientInjected() { _gameClientInfoBar?.close(); - showInfoBar( + showRebootInfoBar( translations.gameClientStarted, severity: InfoBarSeverity.success, duration: infoBarLongDuration @@ -399,10 +412,15 @@ class _LaunchButtonState extends State { } Future _onGameServerInjected() async { - _gameServerInfoBar?.close(); - final theme = FluentTheme.of(appKey.currentContext!); + if(_gameServerInfoBar != null) { + _gameServerInfoBar?.close(); + }else { + _gameClientInfoBar?.close(); + } + + final theme = FluentTheme.of(appNavigatorKey.currentContext!); try { - _gameServerInfoBar = showInfoBar( + _gameServerInfoBar = showRebootInfoBar( translations.waitingForGameServer, loading: true, duration: null @@ -414,17 +432,17 @@ class _LaunchButtonState extends State { ); _gameServerInfoBar?.close(); if (!localPingResult) { - showInfoBar( + showRebootInfoBar( translations.gameServerStartWarning, severity: InfoBarSeverity.error, duration: infoBarLongDuration ); return; } - _backendController.joinLocalHost(); + _backendController.joinLocalhost(); final accessible = await _checkGameServer(theme, gameServerPort); if (!accessible) { - showInfoBar( + showRebootInfoBar( translations.gameServerStartLocalWarning, severity: InfoBarSeverity.warning, duration: infoBarLongDuration @@ -436,7 +454,7 @@ class _LaunchButtonState extends State { _gameController.username.text, _hostingController.instance.value!.versionName, ); - showInfoBar( + showRebootInfoBar( translations.gameServerStarted, severity: InfoBarSeverity.success, duration: infoBarLongDuration @@ -448,7 +466,7 @@ class _LaunchButtonState extends State { Future _checkGameServer(FluentThemeData theme, String gameServerPort) async { try { - _gameServerInfoBar = showInfoBar( + _gameServerInfoBar = showRebootInfoBar( translations.checkingGameServer, loading: true, duration: null @@ -464,12 +482,10 @@ class _LaunchButtonState extends State { "$publicIp:$gameServerPort", timeout: const Duration(days: 365) ); - _gameServerInfoBar = showInfoBar( + _gameServerInfoBar = showRebootInfoBar( translations.checkGameServerFixMessage(gameServerPort), action: Button( - onPressed: () async { - pageIndex.value = RebootPageType.info.index; - }, + onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"), child: Text(translations.checkGameServerFixAction), ), severity: InfoBarSeverity.warning, @@ -486,7 +502,6 @@ class _LaunchButtonState extends State { if(host == null) { await _operation?.cancel(); _operation = null; - await _backendController.worker?.cancel(); } host = host ?? widget.host; @@ -542,14 +557,14 @@ class _LaunchButtonState extends State { case _StopReason.normal: break; case _StopReason.missingVersionError: - showInfoBar( + showRebootInfoBar( translations.missingVersionError, severity: InfoBarSeverity.error, duration: infoBarLongDuration, ); break; case _StopReason.missingExecutableError: - showInfoBar( + showRebootInfoBar( translations.missingExecutableError, severity: InfoBarSeverity.error, duration: infoBarLongDuration, @@ -557,7 +572,7 @@ class _LaunchButtonState extends State { break; case _StopReason.exitCode: if(instance != null && !instance.launched) { - showInfoBar( + showRebootInfoBar( translations.corruptedVersionError, severity: InfoBarSeverity.error, duration: infoBarLongDuration, @@ -565,28 +580,39 @@ class _LaunchButtonState extends State { } break; case _StopReason.corruptedVersionError: - showInfoBar( + showRebootInfoBar( translations.corruptedVersionError, severity: InfoBarSeverity.error, duration: infoBarLongDuration, ); break; case _StopReason.corruptedDllError: - showInfoBar( - translations.corruptedDllError(error!), + showRebootInfoBar( + translations.corruptedDllError(error ?? translations.unknownError), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + break; + case _StopReason.missingCustomDllError: + showRebootInfoBar( + translations.missingCustomDllError(error!), severity: InfoBarSeverity.error, duration: infoBarLongDuration, ); break; case _StopReason.tokenError: - showInfoBar( - translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"), + showRebootInfoBar( + translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none), severity: InfoBarSeverity.error, duration: infoBarLongDuration, + action: Button( + onPressed: () => launchUrl(launcherLogFile.uri), + child: Text(translations.openLog), + ) ); break; case _StopReason.unknownError: - showInfoBar( + showRebootInfoBar( translations.unknownFortniteError(error ?? translations.unknownError), severity: InfoBarSeverity.error, duration: infoBarLongDuration, @@ -610,7 +636,8 @@ class _LaunchButtonState extends State { if(dllPath == null) { log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist"); _onStop( - reason: _StopReason.corruptedDllError, + reason: _StopReason.missingCustomDllError, + error: injectable.name, host: hosting ); return; @@ -631,33 +658,62 @@ class _LaunchButtonState extends State { } } - Future _getDllFileOrStop(InjectableDll injectable, bool host) async { + Future _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async { log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}..."); - final path = injectable.path; - log("[${host ? 'HOST' : 'GAME'}] Path: $path"); - final file = File(path); + final (file, customDll) = _settingsController.getInjectableData(injectable); + log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll"); if(await file.exists()) { log("[${host ? 'HOST' : 'GAME'}] Path exists"); return file; } + log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist"); + if(customDll || isRetry) { + log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery"); + return null; + } + log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again..."); - await downloadCriticalDllInteractive(path); + await _settingsController.downloadCriticalDllInteractive(file.path); log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); - return _getDllFileOrStop(injectable, host); + return _getDllFileOrStop(injectable, host, true); } - InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showInfoBar( - translations.launchingHeadlessServer, + InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar( + translations.launchingGameServer, loading: true, duration: null ); - InfoBarEntry _showLaunchingGameClientWidget() => _gameClientInfoBar = showInfoBar( - translations.launchingGameClient, + InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) { + return _gameClientInfoBar = showRebootInfoBar( + linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly, loading: true, - duration: null + duration: null, + action: Obx(() { + if(_hostingController.started.value || linkedHosting) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only( + bottom: 2.0 + ), + child: Button( + onPressed: () async { + _backendController.joinLocalhost(); + if(!_hostingController.started.value) { + _gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true); + _gameClientInfoBar?.close(); + _showLaunchingGameClientWidget(version, hostType, true); + } + }, + child: Text(translations.startGameServer), + ), + ); + }) ); + } } enum _StopReason { @@ -665,6 +721,7 @@ enum _StopReason { missingVersionError, missingExecutableError, corruptedVersionError, + missingCustomDllError, corruptedDllError, backendError, matchmakerError, diff --git a/gui/lib/src/widget/profile_tile.dart b/gui/lib/src/widget/profile_tile.dart index 8c4ae4c..82a2bf9 100644 --- a/gui/lib/src/widget/profile_tile.dart +++ b/gui/lib/src/widget/profile_tile.dart @@ -2,11 +2,12 @@ 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/settings_controller.dart'; -import 'package:reboot_launcher/src/dialog/implementation/profile.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; +import 'package:reboot_launcher/src/messenger/implementation/profile.dart'; class ProfileWidget extends StatefulWidget { - const ProfileWidget({Key? key}) : super(key: key); + final GlobalKey overlayKey; + const ProfileWidget({required this.overlayKey}); @override State createState() => _ProfileWidgetState(); @@ -14,14 +15,13 @@ class ProfileWidget extends StatefulWidget { class _ProfileWidgetState extends State { final GameController _gameController = Get.find(); - final SettingsController _settingsController = Get.find(); @override - Widget build(BuildContext context) => Obx(() { - final firstRun = _settingsController.firstRun.value; - return HoverButton( + Widget build(BuildContext context) => OverlayTarget( + key: widget.overlayKey, + child: HoverButton( margin: const EdgeInsets.all(8.0), - onPressed: firstRun ? null : () async { + onPressed: () async { if(await showProfileForm(context)) { setState(() {}); } @@ -78,8 +78,8 @@ class _ProfileWidgetState extends State { ), ), ) - ); - }); + ), + ); String get _username { var username = _gameController.username.text; diff --git a/gui/lib/src/widget/server_start_button.dart b/gui/lib/src/widget/server_start_button.dart index 2292547..81da42b 100644 --- a/gui/lib/src/widget/server_start_button.dart +++ b/gui/lib/src/widget/server_start_button.dart @@ -4,7 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; -import 'package:reboot_launcher/src/dialog/implementation/server.dart'; +import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/util/translations.dart'; class ServerButton extends StatefulWidget { diff --git a/gui/lib/src/widget/server_type_selector.dart b/gui/lib/src/widget/server_type_selector.dart index 3a3fee5..62854a4 100644 --- a/gui/lib/src/widget/server_type_selector.dart +++ b/gui/lib/src/widget/server_type_selector.dart @@ -2,12 +2,13 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; -import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/util/translations.dart'; class ServerTypeSelector extends StatefulWidget { - const ServerTypeSelector({Key? key}) - : super(key: key); + final Key overlayKey; + const ServerTypeSelector({required this.overlayKey}); @override State createState() => _ServerTypeSelectorState(); @@ -18,12 +19,16 @@ class _ServerTypeSelectorState extends State { @override Widget build(BuildContext context) { - return Obx(() => DropDownButton( - onOpen: () => inDialog = true, - leading: Text(_controller.type.value.label), - items: ServerType.values - .map((type) => _createItem(type)) - .toList() + return Obx(() => OverlayTarget( + key: widget.overlayKey, + child: DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text(_controller.type.value.label), + items: ServerType.values + .map((type) => _createItem(type)) + .toList() + ), )); } diff --git a/gui/lib/src/widget/setting_tile.dart b/gui/lib/src/widget/setting_tile.dart index 8a94157..9b938a0 100644 --- a/gui/lib/src/widget/setting_tile.dart +++ b/gui/lib/src/widget/setting_tile.dart @@ -1,11 +1,12 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/page/pages.dart'; import 'package:skeletons/skeletons.dart'; -class SettingTile extends StatelessWidget { +class SettingTile extends StatefulWidget { + static const double kDefaultHeight = 80.0; static const double kDefaultContentWidth = 200.0; - static const double kDefaultHeaderHeight = 72; final void Function()? onPressed; final Icon icon; @@ -13,28 +14,38 @@ class SettingTile extends StatelessWidget { final Text? subtitle; final Widget? content; final double? contentWidth; + final Key? overlayKey; final List? children; const SettingTile({ + super.key, this.onPressed, required this.icon, required this.title, required this.subtitle, this.content, this.contentWidth = kDefaultContentWidth, + this.overlayKey, this.children }); - + @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only( - bottom: 4.0 - ), - child: HoverButton( - onPressed: _buildOnPressed(context), - builder: (context, states) => Container( - height: 80, + State createState() => SettingTileState(); +} + +class SettingTileState extends State { + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only( + bottom: 4.0 + ), + child: HoverButton( + onPressed: _buildOnPressed(), + builder: (context, states) => ConstrainedBox( + constraints: BoxConstraints( + minHeight: SettingTile.kDefaultHeight + ), + child: Container( width: double.infinity, decoration: BoxDecoration( color: ButtonThemeData.uncheckedInputColor( @@ -42,76 +53,92 @@ class SettingTile extends StatelessWidget { states, transparentWhenNone: true, ), - borderRadius: BorderRadius.all(Radius.circular(4.0)) + borderRadius: BorderRadius.all(Radius.circular(6.0)) ), - child: Card( - borderRadius: const BorderRadius.all( - Radius.circular(4.0) - ), - child: Padding( - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 12.0, - vertical: 6.0 - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - icon, - const SizedBox(width: 16.0), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title == null ? _skeletonTitle : title!, - subtitle == null ? _skeletonSubtitle : subtitle!, - ], - ), - const Spacer(), - _trailing - ], - ), - ) - ) + child: _buildBody() ), ), + ), + ); + + Card _buildBody() { + return Card( + borderRadius: const BorderRadius.all( + Radius.circular(6.0) + ), + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 12.0 + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if(widget.overlayKey != null) + OverlayTarget( + key: widget.overlayKey, + child: widget.icon, + ) + else + widget.icon, + const SizedBox(width: 16.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.title == null ? _skeletonTitle : widget.title!, + widget.subtitle == null ? _skeletonSubtitle : widget.subtitle!, + ], + ), + const Spacer(), + _trailing + ], + ), + ) ); } - void Function()? _buildOnPressed(BuildContext context) { - if(onPressed != null) { - return onPressed; + void Function()? _buildOnPressed() { + if(widget.onPressed != null) { + return widget.onPressed; } - final children = this.children; + final children = this.widget.children; if (children == null) { return null; } - return () async { - await Navigator.of(context).push(PageRouteBuilder( - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - settings: RouteSettings( - name: title?.data - ), - pageBuilder: (context, incoming, outgoing) => ListView.builder( - itemCount: children.length, - itemBuilder: (context, index) => children[index] - ) - )); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) => pageIndex.value = pageIndex.value); - }; + return () => openNestedPage(); + } + + Future openNestedPage() async { + final children = this.widget.children; + if (children == null) { + return; + } + + await Navigator.of(context).push(PageRouteBuilder( + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + settings: RouteSettings( + name: widget.title?.data + ), + pageBuilder: (context, incoming, outgoing) => ListView.builder( + itemCount: children.length, + itemBuilder: (context, index) => children[index] + ) + )); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => pageIndex.value = pageIndex.value); } Widget get _trailing { - final hasContent = content != null; - final hasChildren = children?.isNotEmpty == true; - final hasListener = onPressed != null; + final hasContent = widget.content != null; + final hasChildren = widget.children?.isNotEmpty == true; + final hasListener = widget.onPressed != null; if(hasContent && hasChildren) { return Row( children: [ SizedBox( - width: contentWidth, - child: content + width: widget.contentWidth, + child: widget.content ), const SizedBox(width: 16.0), Icon( @@ -123,8 +150,8 @@ class SettingTile extends StatelessWidget { if (hasContent) { return SizedBox( - width: contentWidth, - child: content + width: widget.contentWidth, + child: widget.content ); } diff --git a/gui/lib/src/widget/version_name_input.dart b/gui/lib/src/widget/version_name_input.dart deleted file mode 100644 index 4fd5738..0000000 --- a/gui/lib/src/widget/version_name_input.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:get/get.dart'; -import 'package:reboot_launcher/src/controller/game_controller.dart'; -import 'package:reboot_launcher/src/util/checks.dart'; -import 'package:reboot_launcher/src/util/translations.dart'; - -class VersionNameInput extends StatelessWidget { - final GameController _gameController = Get.find(); - final TextEditingController controller; - - VersionNameInput({Key? key, required this.controller}) : super(key: key); - - @override - Widget build(BuildContext context) => InfoLabel( - label: translations.versionName, - child: TextFormBox( - controller: controller, - placeholder: translations.versionNameLabel, - autofocus: true, - validator: (version) => checkVersion(version, _gameController.versions.value), - autovalidateMode: AutovalidateMode.onUserInteraction - ), - ); -} diff --git a/gui/lib/src/widget/version_selector.dart b/gui/lib/src/widget/version_selector.dart index 7806499..e47fef7 100644 --- a/gui/lib/src/widget/version_selector.dart +++ b/gui/lib/src/widget/version_selector.dart @@ -1,31 +1,25 @@ import 'dart:async'; -import 'dart:io'; -import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; 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/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/messenger/abstract/dialog.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/messenger/implementation/version.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -import 'package:reboot_launcher/src/widget/add_local_version.dart'; -import 'package:reboot_launcher/src/widget/add_server_version.dart'; -import 'package:reboot_launcher/src/widget/file_selector.dart'; -import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:url_launcher/url_launcher.dart'; class VersionSelector extends StatefulWidget { const VersionSelector({Key? key}) : super(key: key); - static Future openDownloadDialog() => showAppDialog( - builder: (context) => const AddServerVersion(), - ); - - static Future openAddDialog() => showAppDialog( - builder: (context) => const AddLocalVersion(), + static Future openDownloadDialog({bool closable = true}) => showRebootDialog( + builder: (context) => AddVersionDialog( + closable: closable, + ), + dismissWithEsc: closable ); @override @@ -48,7 +42,7 @@ class _VersionSelectorState extends State { onOpen: () => inDialog = true, onClose: () => inDialog = false, leading: Text( - _gameController.selectedVersion?.name ?? translations.selectVersion, + _gameController.selectedVersion?.content.toString() ?? translations.selectVersion, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -58,30 +52,6 @@ class _VersionSelectorState extends State { ); }); - List _createSelectorItems(BuildContext context) { - final items = _gameController.versions.value - .map((version) => _createVersionItem(context, version)) - .toList(); - items.add(MenuFlyoutItem( - text: Text(translations.addLocalBuildContent), - onPressed: VersionSelector.openAddDialog - )); - items.add(MenuFlyoutItem( - text: Text(translations.downloadBuildContent), - onPressed: VersionSelector.openDownloadDialog - )); - return items; - } - - MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem( - text: _createOptionsMenu( - version: version, - close: true, - child: Text(version.name), - ), - onPressed: () => _gameController.selectedVersion = version - ); - Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) => Listener( onPointerDown: (event) async { if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) { @@ -104,13 +74,64 @@ class _VersionSelectorState extends State { child: child ); + List _createSelectorItems(BuildContext context) { + final items = _gameController.versions.value + .map((version) => _createVersionItem(context, version)) + .toList(); + items.add(MenuFlyoutItem( + trailing: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + FluentIcons.add_24_regular, + size: 12 + ), + ), + text: Text(translations.addVersion), + onPressed: VersionSelector.openDownloadDialog + )); + return items; + } + + MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem( + text: Listener( + onPointerDown: (event) async { + if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) { + return; + } + + await _openVersionOptions(version); + }, + child: Text(version.content.toString()) + ), + trailing: IconButton( + onPressed: () => _openVersionOptions(version), + icon: Icon( + FluentIcons.more_vertical_24_regular + ) + ), + onPressed: () => _gameController.selectedVersion = version + ); + + Future _openVersionOptions(FortniteVersion version) async { + final result = await _flyoutController.showFlyout<_ContextualOption?>( + builder: (context) => MenuFlyout( + items: _ContextualOption.values + .map((entry) => _createOption(context, entry)) + .toList() + ), + barrierDismissible: true, + barrierColor: Colors.transparent + ); + _handleResult(result, version, true); + } + void _handleResult(_ContextualOption? result, FortniteVersion version, bool close) async { + if(!mounted){ + return; + } + switch (result) { case _ContextualOption.openExplorer: - if(!mounted){ - return; - } - if(close) { Navigator.of(context).pop(); } @@ -118,23 +139,8 @@ class _VersionSelectorState extends State { launchUrl(version.location.uri) .onError((error, stackTrace) => _onExplorerError()); break; - case _ContextualOption.modify: - if(!mounted){ - return; - } - - if(close) { - Navigator.of(context).pop(); - } - - await _openRenameDialog(context, version); - break; case _ContextualOption.delete: - if(!mounted){ - return; - } - - var result = await _openDeleteDialog(context, version) ?? false; + final result = await _openDeleteDialog(context, version) ?? false; if(!mounted || !result){ return; } @@ -149,25 +155,25 @@ class _VersionSelectorState extends State { } break; - default: + case null: break; } } MenuFlyoutItem _createOption(BuildContext context, _ContextualOption entry) { return MenuFlyoutItem( - text: Text(entry.name), + text: Text(entry.translatedName), onPressed: () => Navigator.of(context).pop(entry) ); } bool _onExplorerError() { - showInfoBar(translations.missingVersion); + showRebootInfoBar(translations.missingVersion); return false; } Future _openDeleteDialog(BuildContext context, FortniteVersion version) { - return showAppDialog( + return showRebootDialog( builder: (context) => ContentDialog( content: Column( mainAxisSize: MainAxisSize.min, @@ -189,87 +195,32 @@ class _VersionSelectorState extends State { ], ), actions: [ - Button( - onPressed: () => Navigator.of(context).pop(false), - child: Text(translations.deleteVersionCancel), + DialogButton( + type: ButtonType.secondary, + onTap: () => Navigator.of(context).pop(false), + text: translations.deleteVersionCancel ), - Button( - onPressed: () => Navigator.of(context).pop(true), - child: Text(translations.deleteVersionConfirm), + DialogButton( + type: ButtonType.primary, + onTap: () => Navigator.of(context).pop(true), + text: translations.deleteVersionConfirm ) ], ) ); } - - Future _openRenameDialog(BuildContext context, FortniteVersion version) { - var nameController = TextEditingController(text: version.name); - var pathController = TextEditingController(text: version.location.path); - return showAppDialog( - builder: (context) => FormDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InfoLabel( - label: translations.versionName, - child: TextFormBox( - controller: nameController, - placeholder: translations.newVersionNameLabel, - autofocus: true, - validator: (text) => checkChangeVersion(text) - ) - ), - - const SizedBox( - height: 16.0 - ), - - FileSelector( - placeholder: translations.newVersionNameLabel, - windowTitle: translations.gameFolderPlaceWindowTitle, - label: translations.gameFolderLabel, - controller: pathController, - validator: checkGameFolder, - folder: true - ), - - const SizedBox(height: 8.0), - ], - ), - buttons: [ - DialogButton( - type: ButtonType.secondary - ), - - DialogButton( - text: translations.newVersionNameConfirm, - type: ButtonType.primary, - onTap: () { - Navigator.of(context).pop(); - _gameController.updateVersion(version, (version) { - version.name = nameController.text; - version.location = Directory(pathController.text); - }); - }, - ) - ] - ) - ); - } } enum _ContextualOption { openExplorer, - modify, - delete -} + delete; -extension _ContextualOptionExtension on _ContextualOption { - String get name { - return this == _ContextualOption.openExplorer ? translations.openInExplorer - : this == _ContextualOption.modify ? translations.modify - : translations.delete; + String get translatedName { + switch(this) { + case _ContextualOption.openExplorer: + return translations.openInExplorer; + case _ContextualOption.delete: + return translations.delete; + } } -} +} \ No newline at end of file diff --git a/gui/lib/src/widget/version_selector_tile.dart b/gui/lib/src/widget/version_selector_tile.dart index beb4da8..8afa343 100644 --- a/gui/lib/src/widget/version_selector_tile.dart +++ b/gui/lib/src/widget/version_selector_tile.dart @@ -1,10 +1,13 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; +import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart'; -SettingTile get versionSelectSettingTile => SettingTile( +SettingTile buildVersionSelector({ + required GlobalKey key +}) => SettingTile( icon: Icon( FluentIcons.play_24_regular ), @@ -15,6 +18,9 @@ SettingTile get versionSelectSettingTile => SettingTile( constraints: BoxConstraints( minWidth: SettingTile.kDefaultContentWidth, ), - child: const VersionSelector() + child: OverlayTarget( + key: key, + child: const VersionSelector(), + ) ) ); \ No newline at end of file diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 07a3bbd..3d817b4 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Graphical User Interface for Project Reboot -version: "9.1.4" +version: "9.2.0" publish_to: 'none' @@ -31,7 +31,7 @@ dependencies: bitsdojo_window: ^0.1.5 window_manager: ^0.3.8 - # Extract zip archives (for example the reboot.zip coming from github) + # Extract zip archives (for example the reboot.zip) archive: ^3.3.1 # Cryptographic functions @@ -98,6 +98,4 @@ flutter: - assets/backend/profiles/ - assets/backend/public/ - assets/backend/responses/ - - assets/build/ - - assets/info/en/faq/ - - assets/info/en/questions/ \ No newline at end of file + - assets/build/ \ No newline at end of file diff --git a/gui/windows/packaging/exe/custom-inno-setup-script.iss b/gui/windows/packaging/exe/custom-inno-setup-script.iss index 7d170f6..3184436 100644 --- a/gui/windows/packaging/exe/custom-inno-setup-script.iss +++ b/gui/windows/packaging/exe/custom-inno-setup-script.iss @@ -19,6 +19,7 @@ WizardStyle=modern PrivilegesRequired=admin ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64 +ChangesEnvironment=yes [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -47,4 +48,9 @@ function InitializeSetup: Boolean; begin Dependency_AddVC2015To2022 Result := True; -end; \ No newline at end of file +end; + +[Registry] +Root: HKCU; Subkey: "Environment"; ValueType:string; ValueName: "OPENSSL_ia32cap"; \ + ValueData: "~0x20000000"; Flags: preservestringtype +