Reboot v3

This commit is contained in:
Alessandro Autiero
2023-06-03 18:30:50 +02:00
parent 5eafcae616
commit 30f1b0f162
44 changed files with 1041 additions and 613 deletions

BIN
assets/browse/watch.exe Normal file

Binary file not shown.

View File

@@ -1 +1,2 @@
taskkill /f /im build.exe
taskkill /f /im winrar.exe
taskkill /f /im tar.exe

BIN
assets/builds/winrar.exe Normal file

Binary file not shown.

BIN
assets/dlls/reboot.dll Normal file

Binary file not shown.

View File

@@ -73,7 +73,6 @@ void main(List<String> args) async {
}
await patchHeadless(version.executable!);
await patchMatchmaking(version.executable!);
var serverType = getServerType(result);
var serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"];

View File

@@ -14,50 +14,60 @@ import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/page/home_page.dart';
import 'package:reboot_launcher/supabase.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:system_theme/system_theme.dart';
const double kDefaultWindowWidth = 885;
const double kDefaultWindowHeight = 885;
const double kDefaultWindowWidth = 1024;
const double kDefaultWindowHeight = 1024;
final GlobalKey appKey = GlobalKey();
void main() async {
await installationDirectory.create(recursive: true);
WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load();
await GetStorage.init("reboot_game");
await GetStorage.init("reboot_server");
await GetStorage.init("reboot_update");
await GetStorage.init("reboot_settings");
await GetStorage.init("reboot_hosting");
Get.put(GameController());
Get.put(ServerController());
Get.put(BuildController());
Get.put(SettingsController());
Get.put(HostingController());
doWhenWindowReady(() {
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
var controller = Get.find<SettingsController>();
var size = Size(controller.width, controller.height);
var window = appWindow as WinDesktopWindow;
window.setWindowCutOnMaximize(appBarSize * 2);
appWindow.size = size;
if(controller.offsetX != null && controller.offsetY != null){
appWindow.position = Offset(controller.offsetX!, controller.offsetY!);
}else {
appWindow.alignment = Alignment.center;
}
runZonedGuarded(() async {
await installationDirectory.create(recursive: true);
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey
);
WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load();
await GetStorage.init("reboot_game");
await GetStorage.init("reboot_server");
await GetStorage.init("reboot_update");
await GetStorage.init("reboot_settings");
await GetStorage.init("reboot_hosting");
var gameController = GameController();
Get.put(gameController);
Get.put(ServerController());
Get.put(BuildController());
Get.put(SettingsController());
Get.put(HostingController());
doWhenWindowReady(() {
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
var controller = Get.find<SettingsController>();
var size = Size(controller.width, controller.height);
var window = appWindow as WinDesktopWindow;
window.setWindowCutOnMaximize(appBarSize * 2);
appWindow.size = size;
if(controller.offsetX != null && controller.offsetY != null){
appWindow.position = Offset(controller.offsetX!, controller.offsetY!);
}else {
appWindow.alignment = Alignment.center;
}
appWindow.title = "Reboot Launcher";
appWindow.show();
});
runZonedGuarded(
() async => runApp(const RebootApplication()),
(error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
)
);
appWindow.title = "Reboot Launcher";
appWindow.show();
});
var supabase = Supabase.instance.client;
await supabase.from('hosts')
.delete()
.match({'id': gameController.uuid});
runApp(const RebootApplication());
},
(error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
));
}
class RebootApplication extends StatefulWidget {
@@ -70,21 +80,18 @@ class RebootApplication extends StatefulWidget {
class _RebootApplicationState extends State<RebootApplication> {
@override
Widget build(BuildContext context) => FluentApp(
title: "Reboot Launcher",
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
color: SystemTheme.accentColor.accent.toAccentColor(),
darkTheme: _createTheme(Brightness.dark),
theme: _createTheme(Brightness.light),
home: HomePage(key: appKey),
title: "Reboot Launcher",
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
color: SystemTheme.accentColor.accent.toAccentColor(),
darkTheme: _createTheme(Brightness.dark),
theme: _createTheme(Brightness.light),
home: const HomePage()
);
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
brightness: brightness,
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
visualDensity: VisualDensity.standard,
focusTheme: FocusThemeData(
glowFactor: is10footScreen() ? 2.0 : 0.0,
),
brightness: brightness,
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
visualDensity: VisualDensity.standard
);
}

View File

@@ -1,8 +1,6 @@
import 'dart:io';
import 'package:process_run/shell.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_proxy/shelf_proxy.dart';
import '../model/server_type.dart';
import '../util/server.dart' as server;
@@ -62,7 +60,7 @@ Future<HttpServer?> _changeReverseProxyState(String host, String port) async {
return null;
}
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
return await server.startRemoteServer(uri);
}catch(error){
throw Exception("Cannot start reverse proxy");
}

View File

@@ -4,15 +4,19 @@ class GameInstance {
final Process gameProcess;
final Process? launcherProcess;
final Process? eacProcess;
final int? watchDogProcessPid;
bool tokenError;
bool hasChildServer;
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess, this.hasChildServer)
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess, this.watchDogProcessPid, this.hasChildServer)
: tokenError = false;
void kill() {
gameProcess.kill(ProcessSignal.sigabrt);
launcherProcess?.kill(ProcessSignal.sigabrt);
eacProcess?.kill(ProcessSignal.sigabrt);
if(watchDogProcessPid != null){
Process.killPid(watchDogProcessPid!, ProcessSignal.sigabrt);
}
}
}

View File

@@ -2,25 +2,18 @@ import 'package:get/get.dart';
import 'package:reboot_launcher/src/model/fortnite_build.dart';
class BuildController extends GetxController {
List<FortniteBuild>? builds;
FortniteBuild? _selectedBuild;
final List<Function()> _listeners;
late RxBool cancelledDownload;
List<FortniteBuild>? _builds;
Rxn<FortniteBuild> selectedBuildRx;
BuildController() : _listeners = [] {
cancelledDownload = RxBool(false);
}
BuildController() : selectedBuildRx = Rxn();
FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0);
List<FortniteBuild>? get builds => _builds;
set selectedBuild(FortniteBuild build) {
_selectedBuild = build;
for (var listener in _listeners) {
listener();
set builds(List<FortniteBuild>? builds) {
_builds = builds;
if(builds == null || builds.isEmpty){
return;
}
selectedBuildRx.value = builds[0];
}
void addOnBuildChangedListener(Function() listener) => _listeners.add(listener);
void removeOnBuildChangedListener() => _listeners.clear();
}

View File

@@ -7,12 +7,14 @@ import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_instance.dart';
import 'package:uuid/uuid.dart';
import '../../model/update_status.dart';
const String kDefaultPlayerName = "Player";
class GameController extends GetxController {
late final String uuid;
late final GetStorage _storage;
late final TextEditingController username;
late final TextEditingController password;
@@ -21,7 +23,6 @@ class GameController extends GetxController {
late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool started;
late final Rx<UpdateStatus> updateStatus;
GameInstance? instance;
GameController() {
@@ -35,6 +36,8 @@ class GameController extends GetxController {
var decodedSelectedVersionName = _storage.read("version");
var decodedSelectedVersion = decodedVersions.firstWhereOrNull(
(element) => element.name == decodedSelectedVersionName);
uuid = _storage.read("uuid") ?? const Uuid().v4();
_storage.write("uuid", uuid);
_selectedVersion = Rxn(decodedSelectedVersion);
username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage.write("username", username.text));
@@ -44,7 +47,6 @@ class GameController extends GetxController {
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? ""));
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false);
updateStatus = Rx(UpdateStatus.waiting);
}
FortniteVersion? getVersionByName(String name) {
@@ -67,6 +69,9 @@ class GameController extends GetxController {
void removeVersion(FortniteVersion version) {
versions.update((val) => val?.remove(version));
if (selectedVersion?.name == version.name || hasNoVersions) {
selectedVersion = null;
}
}
Future<void> _saveVersions() async {
@@ -81,7 +86,7 @@ class GameController extends GetxController {
FortniteVersion? get selectedVersion => _selectedVersion();
set selectedVersion(FortniteVersion? version) {
_selectedVersion(version);
_selectedVersion.value = version;
_storage.write("version", version?.name);
}

View File

@@ -1,8 +1,12 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/controller/update_controller.dart';
import '../../model/game_instance.dart';
import '../../model/update_status.dart';
import '../../util/reboot.dart';
const String kDefaultServerName = "Reboot Game Server";
@@ -10,19 +14,39 @@ const String kDefaultServerName = "Reboot Game Server";
class HostingController extends GetxController {
late final GetStorage _storage;
late final TextEditingController name;
late final TextEditingController category;
late final TextEditingController description;
late final RxBool discoverable;
late final RxBool started;
late final Rx<UpdateStatus> updateStatus;
GameInstance? instance;
HostingController() {
_storage = GetStorage("reboot_hosting");
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
name.addListener(() => _storage.write("name", name.text));
category = TextEditingController(text: _storage.read("category") ?? "");
category.addListener(() => _storage.write("category", category.text));
description = TextEditingController(text: _storage.read("description") ?? "");
description.addListener(() => _storage.write("description", description.text));
discoverable = RxBool(_storage.read("discoverable") ?? false);
discoverable.listen((value) => _storage.write("discoverable", value));
updateStatus = Rx(UpdateStatus.waiting);
started = RxBool(false);
startUpdater();
}
Future<void> startUpdater() async {
var settings = Get.find<SettingsController>();
if(!settings.autoUpdate()){
updateStatus.value = UpdateStatus.success;
return;
}
updateStatus.value = UpdateStatus.started;
try {
updateTime = await downloadRebootDll(settings.updateUrl.text, updateTime);
updateStatus.value = UpdateStatus.success;
}catch(_) {
updateStatus.value = UpdateStatus.error;
rethrow;
}
}
}

View File

@@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/widget/home/version_name_input.dart';
import '../../util/checks.dart';
import '../widget/shared/file_selector.dart';
@@ -38,12 +39,12 @@ class AddLocalVersion extends StatelessWidget {
height: 16.0
),
TextFormBox(
controller: _nameController,
header: "Name",
placeholder: "Type the version's name",
autofocus: true,
validator: (text) => checkVersion(text, _gameController.versions.value)
VersionNameInput(
controller: _nameController
),
const SizedBox(
height: 16.0
),
FileSelector(
@@ -53,6 +54,10 @@ class AddLocalVersion extends StatelessWidget {
controller: _gamePathController,
validator: checkGameFolder,
folder: true
),
const SizedBox(
height: 16.0
)
],
),

View File

@@ -32,16 +32,16 @@ class _AddServerVersionState extends State<AddServerVersion> {
final BuildController _buildController = Get.find<BuildController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _pathController = TextEditingController();
final Rx<DownloadStatus> _status = Rx(DownloadStatus.form);
final GlobalKey<FormState> _formKey = GlobalKey();
final Rxn<String> _timeLeft = Rxn();
final Rxn<double> _downloadProgress = Rxn();
late DiskSpace _diskSpace;
late Future _fetchFuture;
late Future _diskFuture;
DownloadStatus _status = DownloadStatus.form;
String _timeLeft = "00:00:00";
double _downloadProgress = 0;
CancelableOperation? _manifestDownloadProcess;
CancelableOperation? _driveDownloadOperation;
Object? _error;
StackTrace? _stackTrace;
@@ -54,7 +54,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
_diskSpace = DiskSpace();
_diskFuture = _diskSpace.scan()
.then((_) => _updateFormDefaults());
_buildController.addOnBuildChangedListener(() => _updateFormDefaults());
super.initState();
}
@@ -62,57 +61,74 @@ class _AddServerVersionState extends State<AddServerVersion> {
void dispose() {
_pathController.dispose();
_nameController.dispose();
_buildController.removeOnBuildChangedListener();
_onDisposed();
_cancelDownload();
super.dispose();
}
void _onDisposed() {
if (_status != DownloadStatus.downloading) {
void _cancelDownload() {
if (_status.value != DownloadStatus.extracting && _status.value != DownloadStatus.extracting) {
return;
}
if (_manifestDownloadProcess != null) {
_manifestDownloadProcess?.cancel();
_buildController.cancelledDownload(true);
if (_manifestDownloadProcess == null) {
return;
}
if (_driveDownloadOperation == null) {
return;
}
_driveDownloadOperation!.cancel();
_buildController.cancelledDownload(true);
Process.run('${assetsDirectory.path}\\builds\\stop.bat', []);
_manifestDownloadProcess?.cancel();
}
@override
Widget build(BuildContext context) {
switch(_status){
case DownloadStatus.form:
return _createFormDialog();
case DownloadStatus.downloading:
return GenericDialog(
header: _createDownloadBody(),
buttons: _createCloseButton()
);
case DownloadStatus.extracting:
return GenericDialog(
header: _createExtractingBody(),
buttons: _createCloseButton()
);
case DownloadStatus.error:
return ErrorDialog(
exception: _error ?? Exception("unknown error"),
stackTrace: _stackTrace,
errorMessageBuilder: (exception) => "Cannot download version: $exception"
);
case DownloadStatus.done:
return const InfoDialog(
text: "The download was completed successfully!",
);
}
}
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: "Fetching builds and disks...",
onStop: () => Navigator.of(context).pop()
);
}
return FormDialog(
content: _createFormBody(),
buttons: _createFormButtons()
);
}
);
case DownloadStatus.downloading:
return GenericDialog(
header: _createDownloadBody(),
buttons: _createCloseButton()
);
case DownloadStatus.extracting:
return GenericDialog(
header: _createExtractingBody(),
buttons: _createCloseButton()
);
case DownloadStatus.error:
return ErrorDialog(
exception: _error ?? Exception("unknown error"),
stackTrace: _stackTrace,
errorMessageBuilder: (exception) => "Cannot download version: $exception"
);
case DownloadStatus.done:
return const InfoDialog(
text: "The download was completed successfully!",
);
}
})
);
List<DialogButton> _createFormButtons() {
return [
@@ -127,61 +143,56 @@ class _AddServerVersionState extends State<AddServerVersion> {
void _startDownload(BuildContext context) async {
try {
setState(() => _status = DownloadStatus.downloading);
var build = _buildController.selectedBuildRx.value;
if(build == null){
return;
}
_status.value = DownloadStatus.downloading;
var future = downloadArchiveBuild(
_buildController.selectedBuild.link,
build.link,
Directory(_pathController.text),
_onDownloadProgress,
_onUnrar
(progress, eta) => _onDownloadProgress(progress, eta, false),
(progress, eta) => _onDownloadProgress(progress, eta, true),
);
future.then((value) => _onDownloadComplete());
future.onError((error, stackTrace) => _onDownloadError(error, stackTrace));
_manifestDownloadProcess = CancelableOperation.fromFuture(future);
} catch (exception, stackTrace) {
_onDownloadError(exception, stackTrace);
}
}
void _onUnrar() {
setState(() => _status = DownloadStatus.extracting);
}
Future<void> _onDownloadComplete() async {
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.done;
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_pathController.text)
));
});
_status.value = DownloadStatus.done;
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_pathController.text)
));
}
void _onDownloadError(Object? error, StackTrace? stackTrace) {
print("Error");
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.error;
_error = error;
_stackTrace = stackTrace;
});
_status.value = DownloadStatus.error;
_error = error;
_stackTrace = stackTrace;
}
void _onDownloadProgress(double progress, String timeLeft) {
void _onDownloadProgress(double? progress, String? timeLeft, bool extracting) {
if (!mounted) {
return;
}
setState(() {
_status = DownloadStatus.downloading;
_timeLeft = timeLeft;
_downloadProgress = progress;
});
_status.value = extracting ? DownloadStatus.extracting : DownloadStatus.downloading;
_timeLeft.value = timeLeft;
_downloadProgress.value = progress;
}
Widget _createDownloadBody() => Column(
@@ -204,14 +215,15 @@ class _AddServerVersionState extends State<AddServerVersion> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${_downloadProgress.round()}%",
"${(_downloadProgress.value ?? 0).round()}%",
style: FluentTheme.maybeOf(context)?.typography.body,
),
Text(
"Time left: $_timeLeft",
style: FluentTheme.maybeOf(context)?.typography.body,
)
if(_timeLeft.value != null)
Text(
"Time left: ${_timeLeft.value}",
style: FluentTheme.maybeOf(context)?.typography.body,
)
],
),
@@ -221,7 +233,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
SizedBox(
width: double.infinity,
child: ProgressBar(value: _downloadProgress.toDouble())
child: ProgressBar(value: (_downloadProgress.value ?? 0).toDouble())
),
const SizedBox(
@@ -257,39 +269,27 @@ class _AddServerVersionState extends State<AddServerVersion> {
],
);
Widget _createFormDialog() {
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: "Fetching builds and disks...",
onStop: () => Navigator.of(context).pop()
);
}
return FormDialog(
content: _createFormBody(),
buttons: _createFormButtons()
);
}
);
}
Widget _createFormBody() {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const BuildSelector(),
const SizedBox(height: 20.0),
VersionNameInput(controller: _nameController),
BuildSelector(
onSelected: _updateFormDefaults
),
const SizedBox(
height: 16.0
),
VersionNameInput(
controller: _nameController
),
const SizedBox(
height: 16.0
),
FileSelector(
label: "Path",
placeholder: "Type the download destination",
@@ -298,6 +298,10 @@ class _AddServerVersionState extends State<AddServerVersion> {
validator: checkDownloadDestination,
folder: true
),
const SizedBox(
height: 16.0
)
],
);
}
@@ -319,9 +323,15 @@ class _AddServerVersionState extends State<AddServerVersion> {
await _fetchFuture;
var bestDisk = _diskSpace.disks
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
var build = _buildController.selectedBuildRx.value;
if(build== null){
return;
}
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite "
"${_buildController.selectedBuild.version.toString()}";
_nameController.text = _buildController.selectedBuild.version.toString();
"${build.version}";
_nameController.text = build.version.toString();
_formKey.currentState?.validate();
}
}

View File

@@ -70,7 +70,7 @@ class FormDialog extends AbstractDialog {
text: entry.text,
type: entry.type,
onTap: () {
if(!Form.of(context)!.validate()) {
if(!Form.of(context).validate()) {
return;
}

View File

@@ -5,15 +5,13 @@ import '../../../main.dart';
import 'dialog.dart';
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
"This means that you cannot currently host this version of the game. "
"For a list of supported versions, check #info in the Discord server. "
"If you are unsure which version works best, use build 7.40. "
"If you are a passionate programmer you can add support by opening a PR on Github. ";
const String _corruptedBuildError = "The build you are currently using is corrupted. "
"This means that some critical files are missing for the game to launch. "
"Download the build again from the launcher or, if it's not available there, from another source. "
"Occasionally some files might get corrupted if there isn't enough space on your drive.";
const String _corruptedBuildError = "An unknown error happened while launching Fortnite. "
"Some critical could be missing in your installation. "
"Download the build again from the launcher, not locally, or from a different source. "
"Alternatively, something could have gone wrong in the launcher. ";
Future<void> showBrokenError() async {
showDialog(
@@ -82,7 +80,7 @@ Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? st
builder: (context) => ErrorDialog(
exception: error,
stackTrace: stackTrace,
errorMessageBuilder: (exception) => _corruptedBuildError
errorMessageBuilder: (exception) => "${_corruptedBuildError}Error message: $exception"
)
);
}

View File

@@ -125,11 +125,6 @@ extension ServerControllerDialog on ServerController {
return false;
}
var result = await _showPortTakenDialog(3551);
if (!result) {
return false;
}
await freeLawinPort();
await stop();
return _toggle(newResultType);
@@ -139,11 +134,6 @@ extension ServerControllerDialog on ServerController {
return false;
}
var result = await _showPortTakenDialog(8080);
if (!result) {
return false;
}
await freeMatchmakerPort();
await stop();
return _toggle(newResultType);
@@ -203,14 +193,13 @@ extension ServerControllerDialog on ServerController {
Future<Uri?> _pingRemoteInteractive() async {
try {
var mainFuture = ping(host.text, port.text).then((value) => value != null);
var future = _waitFutureOrTime(mainFuture);
var result = await showDialog<bool>(
var future = ping(host.text, port.text);
await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: future,
closeAutomatically: false,
closeAutomatically: true,
loadingMessage: "Pinging remote server...",
successfulBody: FutureBuilderDialog.ofMessage(
"The server at ${host.text}:${port
@@ -220,8 +209,8 @@ extension ServerControllerDialog on ServerController {
.text} doesn't work. Check the hostname and/or the port and try again."),
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
)
) ?? false;
return result ? await future : null;
);
return await future;
} catch (_) {
return null;
}
@@ -236,27 +225,6 @@ extension ServerControllerDialog on ServerController {
);
}
Future<bool> _showPortTakenDialog(int port) async {
return await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
InfoDialog(
text: "Port $port is already in use, do you want to kill the associated process?",
buttons: [
DialogButton(
type: ButtonType.secondary,
onTap: () => Navigator.of(context).pop(false),
),
DialogButton(
text: "Kill",
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(true),
),
],
)
) ?? false;
}
void _showCannotStopError() {
if(!started.value){
return;
@@ -298,7 +266,7 @@ extension ServerControllerDialog on ServerController {
)
);
void _showIllegalPortError() => showMessage("Illegal port for backend server, use only numbers");
void _showIllegalPortError() => showMessage("Invalid port for backend server");
void _showMissingPortError() => showMessage("Missing port for backend server");

View File

@@ -0,0 +1,113 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class BrowsePage extends StatefulWidget {
const BrowsePage(
{Key? key})
: super(key: key);
@override
State<BrowsePage> createState() => _BrowsePageState();
}
class _BrowsePageState extends State<BrowsePage> with AutomaticKeepAliveClientMixin {
Future? _query;
Stream<List<Map<String, dynamic>>>? _stream;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
if(_query != null) {
return;
}
_query = _stream != null ? Future.value(_stream) : _initStream();
}
Future<void> _initStream() async {
var supabase = Supabase.instance.client;
_stream = supabase.from('hosts')
.stream(primaryKey: ['id'])
.asBroadcastStream();
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: _query,
builder: (context, value) => StreamBuilder<List<Map<String, dynamic>>>(
stream: _stream,
builder: (context, snapshot) {
if(snapshot.hasError){
return Center(
child: Text(
"Cannot fetch servers: ${snapshot.error}",
textAlign: TextAlign.center
)
);
}
var data = snapshot.data;
if(data == null){
return const SizedBox();
}
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Server Browser',
textAlign: TextAlign.start,
style: FluentTheme.of(context).typography.title
),
const SizedBox(
height: 4.0
),
const Text(
'Looking for a match? This is the right place!',
textAlign: TextAlign.start
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
var version = data[index]['version'];
var versionSplit = version.indexOf("-");
version = versionSplit != -1 ? version.substring(0, versionSplit) : version;
version = version.endsWith(".0") ? version.substring(0, version.length - 2) : version;
return SettingTile(
title: "${data[index]['name']} • Fortnite $version",
subtitle: data[index]['description'],
content: Button(
onPressed: () {},
child: const Text('Join'),
)
);
}
),
),
)
],
),
);
}
)
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/ui/page/launcher_page.dart';
import 'package:reboot_launcher/src/ui/page/server_page.dart';
@@ -21,8 +22,9 @@ class HomePage extends StatefulWidget {
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WindowListener {
static const double _defaultPadding = 12.0;
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
static const double _kDefaultPadding = 12.0;
static const int _kPagesLength = 5;
final SettingsController _settingsController = Get.find<SettingsController>();
@@ -32,8 +34,11 @@ class _HomePageState extends State<HomePage> with WindowListener {
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
final RxBool _focused = RxBool(true);
final RxInt _index = RxInt(0);
final RxBool _nestedNavigation = RxBool(false);
final GlobalKey<NavigatorState> _settingsNavigatorKey = GlobalKey();
final List<GlobalKey<NavigatorState>> _navigators = List.generate(_kPagesLength, (index) => GlobalKey());
final List<RxBool> _navigationStatus = List.generate(_kPagesLength, (index) => RxBool(false));
@override
bool get wantKeepAlive => true;
@override
void initState() {
@@ -85,38 +90,45 @@ class _HomePageState extends State<HomePage> with WindowListener {
}
@override
Widget build(BuildContext context) => Obx(() => Stack(
Widget build(BuildContext context) {
super.build(context);
return Stack(
children: [
NavigationView(
paneBodyBuilder: (body) => Padding(
padding: const EdgeInsets.all(_defaultPadding),
child: body
),
appBar: NavigationAppBar(
title: _draggableArea,
actions: WindowTitleBar(focused: _focused()),
leading: _backButton
),
pane: NavigationPane(
selected: _selectedIndex,
onChanged: _onIndexChanged,
displayMode: PaneDisplayMode.auto,
items: _items,
footerItems: _footerItems,
autoSuggestBox: _autoSuggestBox,
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
),
onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: (child, animation) => child
LayoutBuilder(
builder: (context, specs) => Obx(() => NavigationView(
paneBodyBuilder: (pane, body) => Padding(
padding: const EdgeInsets.all(_kDefaultPadding),
child: body
),
appBar: NavigationAppBar(
title: _draggableArea,
actions: WindowTitleBar(focused: _focused()),
leading: _backButton
),
pane: NavigationPane(
key: appKey,
selected: _selectedIndex,
onChanged: _onIndexChanged,
displayMode: specs.biggest.width <= 1536 ? PaneDisplayMode.compact : PaneDisplayMode.open,
items: _items,
footerItems: _footerItems,
autoSuggestBox: _autoSuggestBox,
autoSuggestBoxReplacement: const Icon(FluentIcons.search),
),
onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: (child, animation) => child
))
),
if(_focused() && isWin11)
const WindowBorder()
Obx(() => isWin11 && _focused.value ? const WindowBorder() : const SizedBox())
]
));
);
}
Widget get _backButton => Obx(() {
// ignore: unused_local_variable
var ignored = _nestedNavigation.value;
for(var entry in _navigationStatus){
entry.value;
}
return PaneItem(
icon: const Icon(FluentIcons.back, size: 14.0),
body: const SizedBox.shrink(),
@@ -128,15 +140,20 @@ class _HomePageState extends State<HomePage> with WindowListener {
);
});
void Function()? _onBack() {
var navigator = _settingsNavigatorKey.currentState;
Function()? _onBack() {
var navigator = _navigators[_index.value].currentState;
if(navigator == null || !navigator.mounted || !navigator.canPop()){
return null;
}
var status = _navigationStatus[_index.value];
if(!status.value){
return null;
}
return () async {
Navigator.pop(navigator.context);
_nestedNavigation.value = false;
status.value = false;
};
}
@@ -187,25 +204,25 @@ class _HomePageState extends State<HomePage> with WindowListener {
PaneItem(
title: const Text("Play"),
icon: const Icon(FluentIcons.game),
body: const LauncherPage()
body: LauncherPage(_navigators[0], _navigationStatus[0])
),
PaneItem(
title: const Text("Host"),
icon: const Icon(FluentIcons.server_processes),
body: const HostingPage()
body: HostingPage(_navigators[1], _navigationStatus[1])
),
PaneItem(
title: const Text("Backend"),
icon: const Icon(FluentIcons.user_window),
body: ServerPage()
body: ServerPage(_navigators[2], _navigationStatus[2])
),
PaneItem(
title: const Text("Tutorial"),
icon: const Icon(FluentIcons.info),
body: InfoPage(_settingsNavigatorKey, _nestedNavigation)
body: InfoPage(_navigators[3], _navigationStatus[3])
),
];

View File

@@ -1,35 +1,70 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
import '../../model/update_status.dart';
import '../../util/reboot.dart';
import '../controller/update_controller.dart';
import 'browse_page.dart';
class HostingPage extends StatefulWidget {
const HostingPage(
{Key? key})
: super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation;
const HostingPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override
State<HostingPage> createState() => _HostingPageState();
}
class _HostingPageState extends State<HostingPage> {
class _HostingPageState extends State<HostingPage> with AutomaticKeepAliveClientMixin {
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
@override
Widget build(BuildContext context) => Column(
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() => !_settingsController.autoUpdate() || _hostingController.updateStatus().isDone() ? _body : _updateScreen);
}
Widget get _body => Navigator(
key: widget.navigatorKey,
initialRoute: "home",
onGenerateRoute: (settings) {
var screen = _createScreen(settings.name);
return FluentPageRoute(
builder: (context) => screen,
settings: settings
);
},
);
Widget _createScreen(String? name) {
switch(name){
case "home":
return _homeScreen;
case "browse":
return const BrowsePage();
default:
throw Exception("Unknown page: $name");
}
}
Widget get _homeScreen => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
const SizedBox(
Obx(() => SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("A window will pop up after the game server is started to modify its in-game settings"),
severity: InfoBarSeverity.info
),
),
child: _hostingController.updateStatus.value == UpdateStatus.error ? _updateError :_rebootGuiInfo,
)),
const SizedBox(
height: 16.0
),
@@ -48,12 +83,12 @@ class _HostingPageState extends State<HostingPage> {
)
),
SettingTile(
title: "Category",
subtitle: "The category of your game server",
title: "Description",
subtitle: "The description of your game server",
isChild: true,
content: TextFormBox(
placeholder: "Category",
controller: _hostingController.category
placeholder: "Description",
controller: _hostingController.description
)
),
SettingTile(
@@ -96,10 +131,55 @@ class _HostingPageState extends State<HostingPage> {
)
]
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Browse available servers",
subtitle: "See a list of other game servers that are being hosted",
content: Button(
onPressed: () {
widget.navigatorKey.currentState?.pushNamed('browse');
widget.nestedNavigation.value = true;
},
child: const Text("Browse")
)
),
const Expanded(child: SizedBox()),
const LaunchButton(
host: true
host: true
)
],
);
InfoBar get _rebootGuiInfo => const InfoBar(
title: Text("A window will pop up after the game server is started to modify its in-game settings"),
severity: InfoBarSeverity.info
);
Widget get _updateScreen => const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
],
);
Widget get _updateError => MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: _hostingController.startUpdater,
child: const InfoBar(
title: Text("The reboot dll couldn't be downloaded: click here to try again"),
severity: InfoBarSeverity.info
),
),
);
}

View File

@@ -15,7 +15,7 @@ class InfoPage extends StatefulWidget {
State<InfoPage> createState() => _InfoPageState();
}
class _InfoPageState extends State<InfoPage> {
class _InfoPageState extends State<InfoPage> with AutomaticKeepAliveClientMixin {
final List<String> _elseTitles = [
"Open the home page",
"Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port",
@@ -42,6 +42,9 @@ class _InfoPageState extends State<InfoPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
late final ScrollController _controller;
@override
bool get wantKeepAlive => true;
@override
void initState() {
_controller = ScrollController(initialScrollOffset: _settingsController.scrollingDistance);
@@ -71,8 +74,6 @@ class _InfoPageState extends State<InfoPage> {
);
Widget _createScreen(String? name) {
WidgetsBinding.instance
.addPostFrameCallback((_) => widget.nestedNavigation.value = name != "home");
switch(name){
case "home":
return _homeScreen;
@@ -91,7 +92,10 @@ class _InfoPageState extends State<InfoPage> {
_createCardWidget(
text: "Play on someone else's server",
description: "If one of your friends is hosting a game server, click here",
onClick: () => widget.navigatorKey.currentState?.pushNamed("else")
onClick: () {
widget.navigatorKey.currentState?.pushNamed("else");
widget.nestedNavigation.value = true;
}
),
const SizedBox(
@@ -101,7 +105,10 @@ class _InfoPageState extends State<InfoPage> {
_createCardWidget(
text: "Host your own server",
description: "If you want to create your own server to invite your friends or to play around by yourself, click here",
onClick: () => widget.navigatorKey.currentState?.pushNamed("own")
onClick: () {
widget.navigatorKey.currentState?.pushNamed("own");
widget.nestedNavigation.value = true;
}
)
]
);

View File

@@ -1,90 +1,76 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/page/browse_page.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../model/update_status.dart';
import '../../util/checks.dart';
import '../../util/reboot.dart';
import '../controller/update_controller.dart';
class LauncherPage extends StatefulWidget {
const LauncherPage(
{Key? key})
: super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation;
const LauncherPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override
State<LauncherPage> createState() => _LauncherPageState();
}
class _LauncherPageState extends State<LauncherPage> {
class _LauncherPageState extends State<LauncherPage> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Navigator(
key: widget.navigatorKey,
initialRoute: "home",
onGenerateRoute: (settings) {
var screen = _createScreen(settings.name);
return FluentPageRoute(
builder: (context) => screen,
settings: settings
);
},
);
}
Widget _createScreen(String? name) {
switch(name){
case "home":
return _GamePage(widget.navigatorKey, widget.nestedNavigation);
case "browse":
return const BrowsePage();
default:
throw Exception("Unknown page: $name");
}
}
}
class _GamePage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation;
const _GamePage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override
State<_GamePage> createState() => _GamePageState();
}
class _GamePageState extends State<_GamePage> {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final BuildController _buildController = Get.find<BuildController>();
late final RxBool _showPasswordTrailing = RxBool(_gameController.password.text.isNotEmpty);
@override
void initState() {
if(_gameController.updateStatus() == UpdateStatus.waiting) {
_startUpdater();
_setupBuildWarning();
}
super.initState();
}
void _setupBuildWarning() {
void onCancelWarning() => WidgetsBinding.instance.addPostFrameCallback((_) {
if(!mounted) {
return;
}
showSnackbar(context, const Snackbar(content: Text("Download cancelled")));
_buildController.cancelledDownload(false);
});
_buildController.cancelledDownload.listen((value) => value ? onCancelWarning() : {});
}
Future<void> _startUpdater() async {
if(!_settingsController.autoUpdate()){
_gameController.updateStatus.value = UpdateStatus.success;
return;
}
_gameController.updateStatus.value = UpdateStatus.started;
try {
updateTime = await downloadRebootDll(_settingsController.updateUrl.text, updateTime);
_gameController.updateStatus.value = UpdateStatus.success;
}catch(_) {
_gameController.updateStatus.value = UpdateStatus.error;
rethrow;
}
}
@override
Widget build(BuildContext context) => Obx(() => !_settingsController.autoUpdate() || _gameController.updateStatus().isDone() ? _homePage : _updateScreen);
Widget get _homePage => Column(
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _gameController.updateStatus() == UpdateStatus.error ? _updateError : const SizedBox(),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: SizedBox(height: _gameController.updateStatus() == UpdateStatus.error ? 16.0 : 0.0),
),
SettingTile(
title: "Credentials",
subtitle: "Your in-game login credentials",
@@ -144,7 +130,10 @@ class _LauncherPageState extends State<LauncherPage> {
title: "Browse available servers",
subtitle: "Discover new game servers that fit your play-style",
content: Button(
onPressed: () => launchUrl(Uri.parse("https://google.com/search?q=One+Day+This+Will+Be+Ready")),
onPressed: () {
widget.navigatorKey.currentState?.pushNamed('browse');
widget.nestedNavigation.value = true;
},
child: const Text("Browse")
),
isChild: true
@@ -185,32 +174,4 @@ class _LauncherPageState extends State<LauncherPage> {
)
],
);
Widget get _updateScreen => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
],
);
Widget get _updateError => MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _startUpdater(),
child: const SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("The reboot dll couldn't be downloaded: click here to try again"),
severity: InfoBarSeverity.info
)
),
),
);
}

View File

@@ -3,20 +3,31 @@ import 'package:get/get.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/ui/widget/server/server_button.dart';
import 'package:url_launcher/url_launcher.dart';
import '../widget/shared/setting_tile.dart';
class ServerPage extends StatelessWidget {
class ServerPage extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation;
const ServerPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override
State<ServerPage> createState() => _ServerPageState();
}
class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMixin {
final ServerController _serverController = Get.find<ServerController>();
ServerPage({Key? key}) : super(key: key);
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/ui/widget/shared/file_selector.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -13,24 +14,36 @@ import '../../util/selector.dart';
import '../dialog/dialog.dart';
import '../widget/shared/setting_tile.dart';
class SettingsPage extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
class SettingsPage extends StatefulWidget {
SettingsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingTile(
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> with AutomaticKeepAliveClientMixin {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingTile(
title: "File settings",
subtitle: "This section contains all the settings related to files used by Fortnite",
expandedContent: [
_createFileSetting(
title: "Game server",
description: "This file is injected to create a game server to host matches",
controller: _settingsController.rebootDll
title: "Game server",
description: "This file is injected to create a game server to host matches",
controller: _settingsController.rebootDll
),
_createFileSetting(
title: "Unreal engine console",
@@ -43,115 +56,111 @@ class SettingsPage extends StatelessWidget {
controller: _settingsController.authDll
),
],
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Automatic updates",
subtitle: "Choose whether the launcher and its files should be automatically updated",
contentWidth: null,
content: Obx(() => ToggleSwitch(
checked: _settingsController.autoUpdate(),
onChanged: (value) => _settingsController.autoUpdate.value = value
))
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Custom launch arguments",
subtitle: "Enter additional arguments to use when launching the game",
content: TextFormBox(
placeholder: "Arguments...",
controller: _gameController.customLaunchArgs,
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Create a bug report",
subtitle: "Help me fix bugs by reporting them",
content: Button(
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues/new/choose")),
child: const Text("Report a bug"),
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Reset settings",
subtitle: "Resets the launcher's settings to their default values",
content: Button(
onPressed: () => showDialog(
context: context,
builder: (context) => InfoDialog(
text: "Do you want to reset all settings to their default values? This action is irreversible",
buttons: [
DialogButton(
type: ButtonType.secondary,
text: "Close",
),
DialogButton(
type: ButtonType.primary,
text: "Reset",
onTap: () {
_settingsController.reset();
Navigator.of(context).pop();
},
)
],
)
),
child: const Text("Reset"),
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Version status",
subtitle: "Current version: 7.0",
content: Button(
onPressed: () => launchUrl(installationDirectory.uri),
child: const Text("Show Files"),
)
),
]
);
Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => ListTile(
title: Text(title),
subtitle: Text(description),
trailing: SizedBox(
width: 256,
child: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: "Path",
controller: controller,
validator: checkDll,
autovalidateMode: AutovalidateMode.always
),
),
const SizedBox(
width: 8.0,
),
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: () async {
var selected = await compute(openFilePicker, "dll");
controller.text = selected ?? controller.text;
},
child: const Icon(FluentIcons.open_folder_horizontal),
),
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Automatic updates",
subtitle: "Choose whether the launcher and its files should be automatically updated",
contentWidth: null,
content: Obx(() => ToggleSwitch(
checked: _settingsController.autoUpdate.value,
onChanged: (value) => _settingsController.autoUpdate.value = value
)),
expandedContentSpacing: 0,
expandedContent: [
SettingTile(
title: "Update Mirror",
subtitle: "The URL used to pull the latest update once a day",
content: Obx(() => TextFormBox(
placeholder: "URL",
controller: _settingsController.updateUrl,
enabled: _settingsController.autoUpdate.value,
validator: checkUpdateUrl
)),
isChild: true
)
]
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Custom launch arguments",
subtitle: "Enter additional arguments to use when launching the game",
content: TextFormBox(
placeholder: "Arguments...",
controller: _gameController.customLaunchArgs,
)
],
)
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Create a bug report",
subtitle: "Help me fix bugs by reporting them",
content: Button(
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues")),
child: const Text("Report a bug"),
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Reset settings",
subtitle: "Resets the launcher's settings to their default values",
content: Button(
onPressed: () => showDialog(
context: context,
builder: (context) => InfoDialog(
text: "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
buttons: [
DialogButton(
type: ButtonType.secondary,
text: "Close",
),
DialogButton(
type: ButtonType.primary,
text: "Reset",
onTap: () {
_settingsController.reset();
Navigator.of(context).pop();
},
)
],
)
),
child: const Text("Reset"),
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Version status",
subtitle: "Current version: 8.0",
content: Button(
onPressed: () => launchUrl(installationDirectory.uri),
child: const Text("Show Files"),
)
),
]
);
}
Widget _createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile(
title: title,
subtitle: description,
content: FileSelector(
placeholder: "Path",
windowTitle: "Select a file",
controller: controller,
validator: checkDll,
extension: "dll",
folder: false
),
isChild: true
);
}

View File

@@ -4,8 +4,9 @@ import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
import 'package:reboot_launcher/src/model/fortnite_build.dart';
class BuildSelector extends StatefulWidget {
final Function() onSelected;
const BuildSelector({Key? key}) : super(key: key);
const BuildSelector({Key? key, required this.onSelected}) : super(key: key);
@override
State<BuildSelector> createState() => _BuildSelectorState();
@@ -18,14 +19,20 @@ class _BuildSelectorState extends State<BuildSelector> {
Widget build(BuildContext context) {
return InfoLabel(
label: "Build",
child: ComboBox<FortniteBuild>(
child: Obx(() => ComboBox<FortniteBuild>(
placeholder: const Text('Select a fortnite build'),
isExpanded: true,
items: _createItems(),
value: _buildController.selectedBuild,
onChanged: (value) =>
value == null ? {} : setState(() => _buildController.selectedBuild = value)
)
value: _buildController.selectedBuildRx.value,
onChanged: (value) {
if(value == null){
return;
}
_buildController.selectedBuildRx.value = value;
widget.onSelected();
}
))
);
}

View File

@@ -24,6 +24,7 @@ import 'package:reboot_launcher/src/../main.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
import 'package:reboot_launcher/src/model/game_instance.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../../util/process.dart';
@@ -37,6 +38,7 @@ class LaunchButton extends StatefulWidget {
}
class _LaunchButtonState extends State<LaunchButton> {
static const String _kLoadingRoute = '/loading';
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
final List<String> _corruptedBuildErrors = [
"when 0 bytes remain",
@@ -50,6 +52,7 @@ class _LaunchButtonState extends State<LaunchButton> {
"UOnlineAccountCommon::ForceLogout"
];
final GlobalKey _headlessServerKey = GlobalKey();
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final ServerController _serverController = Get.find<ServerController>();
@@ -116,10 +119,8 @@ class _LaunchButtonState extends State<LaunchButton> {
}
try {
_fail = false;
var version = _gameController.selectedVersion!;
var gamePath = version.executable?.path;
if(gamePath == null){
if(version.executable?.path == null){
showMissingBuildError(version);
_onStop(widget.host);
return;
@@ -131,7 +132,6 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
await compute(patchMatchmaking, version.executable!);
await compute(patchHeadless, version.executable!);
var automaticallyStartedServer = await _startMatchMakingServer();
@@ -141,9 +141,9 @@ class _LaunchButtonState extends State<LaunchButton> {
await _showServerLaunchingWarning();
}
} catch (exception, stacktrace) {
_closeDialogIfOpen(false);
showCorruptedBuildError(widget.host, exception, stacktrace);
_closeLaunchingWidget(false);
_onStop(widget.host);
showCorruptedBuildError(widget.host, exception, stacktrace);
}
}
@@ -152,7 +152,8 @@ class _LaunchButtonState extends State<LaunchButton> {
var launcherProcess = await _createLauncherProcess(version);
var eacProcess = await _createEacProcess(version);
var gameProcess = await _createGameProcess(version.executable!.path, host);
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
var watchDogProcess = _createWatchdogProcess(gameProcess, launcherProcess, eacProcess);
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, watchDogProcess, hasChildServer);
if(host){
_hostingController.instance = instance;
}else{
@@ -161,6 +162,13 @@ class _LaunchButtonState extends State<LaunchButton> {
_injectOrShowError(Injectable.sslBypass, host);
}
int _createWatchdogProcess(Process? gameProcess, Process? launcherProcess, Process? eacProcess) => startBackgroundProcess(
'${assetsDirectory.path}\\browse\\watch.exe',
[_gameController.uuid, _getProcessPid(gameProcess), _getProcessPid(launcherProcess), _getProcessPid(eacProcess)]
);
String _getProcessPid(Process? process) => process?.pid.toString() ?? "-1";
Future<bool> _startMatchMakingServer() async {
if(widget.host){
return false;
@@ -226,33 +234,50 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
_closeDialogIfOpen(false);
_closeLaunchingWidget(false);
_onStop(widget.host);
}
void _closeDialogIfOpen(bool success) {
void _closeLaunchingWidget(bool success) {
var context = _headlessServerKey.currentContext;
if(context == null || !context.mounted){
return;
}
var route = ModalRoute.of(appKey.currentContext!);
if(route == null || route.isCurrent){
return;
}
Navigator.of(appKey.currentContext!).pop(success);
Navigator.of(context).pop(success);
}
Future<void> _showServerLaunchingWarning() async {
var result = await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) => ProgressDialog(
key: _headlessServerKey,
text: "Launching headless server...",
onStop: () =>_onEnd()
onStop: () => Navigator.of(context).pop(false)
)
) ?? false;
if(result){
if(!result){
_onStop(true);
return;
}
_onStop(widget.host);
if(!_hostingController.discoverable.value){
return;
}
var supabase = Supabase.instance.client;
await supabase.from('hosts').insert({
'id': _gameController.uuid,
'name': _hostingController.name.text,
'description': _hostingController.description.text,
'version': _gameController.selectedVersion?.name ?? 'unknown'
});
}
void _onGameOutput(String line, bool host) {
@@ -280,7 +305,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
_fail = true;
_closeDialogIfOpen(false);
_closeLaunchingWidget(false);
_showTokenError(host);
return;
}
@@ -290,7 +315,7 @@ class _LaunchButtonState extends State<LaunchButton> {
_injectOrShowError(Injectable.console, host);
}else {
_injectOrShowError(Injectable.reboot, host)
.then((value) => _closeDialogIfOpen(true));
.then((value) => _closeLaunchingWidget(true));
}
_injectOrShowError(Injectable.memoryFix, host);
@@ -340,6 +365,13 @@ class _LaunchButtonState extends State<LaunchButton> {
}
_setStarted(host, false);
if(host){
var supabase = Supabase.instance.client;
await supabase.from('hosts')
.delete()
.match({'id': _gameController.uuid});
}
}
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
@@ -387,12 +419,8 @@ class _LaunchButtonState extends State<LaunchButton> {
void _onDllFail(File dllPath, bool hosting) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(_fail){
return;
}
_fail = true;
_closeDialogIfOpen(false);
_closeLaunchingWidget(false);
showMissingDllError(path.basename(dllPath.path));
_onStop(hosting);
});

View File

@@ -9,15 +9,16 @@ class VersionNameInput extends StatelessWidget {
VersionNameInput({Key? key, required this.controller}) : super(key: key);
@override
Widget build(BuildContext context) {
return TextFormBox(
header: "Name",
placeholder: "Type the version's name",
controller: controller,
autofocus: true,
validator: _validate,
);
}
Widget build(BuildContext context) => InfoLabel(
label: "Name",
child: TextFormBox(
controller: controller,
placeholder: "Type the version's name",
autofocus: true,
validator: _validate,
autovalidateMode: AutovalidateMode.onUserInteraction
),
);
String? _validate(String? text) {
if (text == null || text.isEmpty) {

View File

@@ -10,6 +10,7 @@ import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart';
import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart';
@@ -135,12 +136,8 @@ class _VersionSelectorState extends State<VersionSelector> {
}
_gameController.removeVersion(version);
if (_gameController.selectedVersion?.name == version.name || _gameController.hasNoVersions) {
_gameController.selectedVersion = null;
}
if (_deleteFilesController.value && await version.location.exists()) {
version.location.delete(recursive: true);
delete(version.location);
}
break;
@@ -213,12 +210,14 @@ class _VersionSelectorState extends State<VersionSelector> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormBox(
controller: nameController,
header: "Name",
placeholder: "Type the new version name",
autofocus: true,
validator: (text) => checkChangeVersion(text)
InfoLabel(
label: "Name",
child: TextFormBox(
controller: nameController,
placeholder: "Type the new version name",
autofocus: true,
validator: (text) => checkChangeVersion(text)
)
),
const SizedBox(
@@ -228,6 +227,7 @@ class _VersionSelectorState extends State<VersionSelector> {
FileSelector(
placeholder: "Type the new game folder",
windowTitle: "Select game folder",
label: "Path",
controller: pathController,
validator: checkGameFolder,
folder: true

View File

@@ -48,28 +48,16 @@ class _FileSelectorState extends State<FileSelector> {
) : _buildBody;
}
Widget get _buildBody => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormBox(
controller: widget.controller,
placeholder: widget.placeholder,
validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
)
Widget get _buildBody => TextFormBox(
controller: widget.controller,
placeholder: widget.placeholder,
validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction,
suffix: !widget.allowNavigator ? null : Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
),
if (widget.allowNavigator)
const SizedBox(width: 16.0),
if (widget.allowNavigator)
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
)
)
],
suffixMode: OverlayVisibilityMode.editing
);
void _onPressed() {

View File

@@ -26,16 +26,24 @@ class SmartInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextFormBox(
if(label != null){
return InfoLabel(
label: label!,
child: _body
);
}
return _body;
}
TextFormBox get _body => TextFormBox(
enabled: enabled,
controller: controller,
header: label,
keyboardType: type,
placeholder: placeholder,
onTap: onTap,
readOnly: readOnly,
autovalidateMode: validatorMode,
validator: validator
);
}
);
}

View File

@@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:archive/archive_io.dart';
import 'package:html/parser.dart' show parse;
@@ -7,7 +9,6 @@ import 'package:reboot_launcher/src/model/fortnite_build.dart';
import 'package:reboot_launcher/src/util/time.dart';
import 'package:reboot_launcher/src/util/version.dart' as parser;
import 'package:path/path.dart' as path;
import 'package:unrar_file/unrar_file.dart';
import 'os.dart';
@@ -44,21 +45,28 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
return results;
}
Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function() onRar) async {
var outputDir = await destination.createTemp("build");
Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function(double?, String?) onDecompress) async {
var outputDir = Directory("${destination.path}\\.build");
outputDir.createSync(recursive: true);
try {
destination.createSync(recursive: true);
var fileName = archiveUrl.substring(archiveUrl.lastIndexOf("/") + 1);
var extension = path.extension(fileName);
var tempFile = File("${outputDir.path}//$fileName");
var startTime = DateTime.now().millisecondsSinceEpoch;
var tempFile = File("${outputDir.path}\\$fileName");
if(tempFile.existsSync()) {
tempFile.deleteSync(recursive: true);
}
var client = http.Client();
var response = await client.send(
http.Request("GET", Uri.parse(archiveUrl)));
var request = http.Request("GET", Uri.parse(archiveUrl));
request.headers['Connection'] = 'Keep-Alive';
var response = await client.send(request);
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
}
var startTime = DateTime.now().millisecondsSinceEpoch;
var length = response.contentLength!;
var received = 0;
var sink = tempFile.openWrite();
@@ -69,17 +77,66 @@ Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Func
onProgress((received / length) * 100, toETA(eta));
return s;
}).pipe(sink);
onRar();
if(extension.toLowerCase() == ".zip"){
await extractFileToDisk(tempFile.path, destination.path);
}else if(extension.toLowerCase() == ".rar") {
await UnrarFile.extract_rar(tempFile.path, destination.path);
} else {
throw Exception("Unknown file extension: $extension");
}
var receiverPort = ReceivePort();
var file = _CompressedFile(extension, tempFile.path, destination.path, receiverPort.sendPort);
Isolate.spawn<_CompressedFile>(_decompress, file);
var completer = Completer();
receiverPort.forEach((element) {
onDecompress(element.progress, element.eta);
if(element.progress != null && element.progress >= 100){
completer.complete(null);
}
});
await completer.future;
delete(outputDir);
} catch(message) {
throw Exception("Cannot download build: $message");
}finally {
outputDir.delete(recursive: true);
}
}
// TODO: Progress report somehow
Future<void> _decompress(_CompressedFile file) async {
try{
file.sendPort.send(_FileUpdate(null, null));
switch (file.extension.toLowerCase()) {
case '.zip':
var process = await Process.start(
'tar',
['-xf', file.tempFile, '-C', file.destination],
mode: ProcessStartMode.inheritStdio
);
await process.exitCode;
break;
case '.rar':
var process = await Process.start(
'${assetsDirectory.path}\\builds\\winrar.exe',
['x', file.tempFile, '*.*', file.destination],
mode: ProcessStartMode.inheritStdio
);
await process.exitCode;
break;
default:
break;
}
file.sendPort.send(_FileUpdate(100, null));
}catch(exception){
rethrow;
}
}
class _CompressedFile {
final String extension;
final String tempFile;
final String destination;
final SendPort sendPort;
_CompressedFile(this.extension, this.tempFile, this.destination, this.sendPort);
}
class _FileUpdate {
final double? progress;
final String? eta;
_FileUpdate(this.progress, this.eta);
}

View File

@@ -68,5 +68,13 @@ String? checkMatchmaking(String? text) {
return "Empty hostname";
}
return null;
}
String? checkUpdateUrl(String? text) {
if (text == null || text.isEmpty) {
return "Empty URL";
}
return null;
}

View File

@@ -3,6 +3,9 @@ import 'package:fluent_ui/fluent_ui.dart';
import '../../../main.dart';
import '../ui/dialog/dialog.dart';
String? lastError;
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
if(exception == null){
return;
@@ -12,6 +15,16 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
return;
}
if(lastError == exception.toString()){
return;
}
lastError = exception.toString();
var route = ModalRoute.of(appKey.currentContext!);
if(route != null && !route.isCurrent){
Navigator.of(appKey.currentContext!).pop(false);
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog(
context: appKey.currentContext!,
builder: (context) =>

View File

@@ -4,11 +4,12 @@ import 'package:win32/win32.dart';
import 'package:ffi/ffi.dart';
import 'dart:ffi';
import 'package:path/path.dart' as path;
const int appBarSize = 2;
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
bool isLocalHost(String host) => host.trim() == "127.0.0.1" || host.trim().toLowerCase() == "localhost" || host.trim() == "0.0.0.0";
bool get isWin11 {
var result = _regex.firstMatch(Platform.operatingSystemVersion)?.group(1);
if(result == null){
@@ -19,6 +20,33 @@ bool get isWin11 {
return intBuild != null && intBuild > 22000;
}
int startBackgroundProcess(String executable, List<String> args) {
var executablePath = TEXT('$executable ${args.map((entry) => '"$entry"').join(" ")}');
var startupInfo = calloc<STARTUPINFO>();
var processInfo = calloc<PROCESS_INFORMATION>();
var success = CreateProcess(
nullptr,
executablePath,
nullptr,
nullptr,
FALSE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
startupInfo,
processInfo
);
if (success == 0) {
var error = GetLastError();
throw Exception("Cannot start process: $error");
}
var pid = processInfo.ref.dwProcessId;
free(startupInfo);
free(processInfo);
return pid;
}
Future<bool> runElevated(String executable, String args) async {
var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();

View File

@@ -12,8 +12,8 @@ final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
Directory? outputDir;
var now = DateTime.now();
try {
var now = DateTime.now();
var lastUpdate = await _getLastUpdate(lastUpdateMs);
var exists = await rebootDllFile.exists();
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) {
@@ -32,6 +32,12 @@ Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
return now.millisecondsSinceEpoch;
}catch(message) {
if(url == rebootDownloadUrl){
var asset = File('${assetsDirectory.path}\\dlls\\reboot.dll');
await rebootDllFile.writeAsBytes(asset.readAsBytesSync());
return now.millisecondsSinceEpoch;
}
throw Exception("Cannot download reboot.zip, invalid zip: $message");
}finally{
if(outputDir != null) {

View File

@@ -36,7 +36,8 @@ Future<void> startServer(bool detached) async {
serverExeFile.path,
[],
workingDirectory: serverDirectory.path,
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal,
runInShell: detached
);
if(!detached) {
serverLogFile.createSync(recursive: true);
@@ -156,7 +157,14 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
);
}
if(int.tryParse(port) == null){
var portNumber = int.tryParse(port);
if(portNumber == null){
return ServerResult(
type: ServerResultType.illegalPortError
);
}
if(isLocalHost(host) && portNumber == 3551 && type == ServerType.remote){
return ServerResult(
type: ServerResultType.illegalPortError
);
@@ -179,9 +187,7 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
);
}
Future<HttpServer> startRemoteServer(Uri uri) async {
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
}
Future<HttpServer> startRemoteServer(Uri uri) async => await serve(proxyHandler(uri), "127.0.0.1", 3551);
class ServerResult {
final int? pid;

2
lib/supabase.dart Normal file
View File

@@ -0,0 +1,2 @@
const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co';
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M';

51
lib/watch.dart Normal file
View File

@@ -0,0 +1,51 @@
import 'dart:io';
import 'package:reboot_launcher/supabase.dart';
import 'package:supabase/supabase.dart';
void main(List<String> args) async {
if(args.length != 4){
stderr.writeln("Wrong args length: $args");
return;
}
var instance = _GameInstance(args[0], int.parse(args[1]), int.parse(args[2]), int.parse(args[3]));
var supabase = SupabaseClient(supabaseUrl, supabaseAnonKey);
while(true){
sleep(const Duration(seconds: 2));
stdout.writeln("Looking up tasks...");
var result = Process.runSync('tasklist', []);
var output = result.stdout.toString();
if(output.contains(" ${instance.gameProcess} ")) {
continue;
}
stdout.writeln("Killing $instance");
Process.killPid(instance.gameProcess, ProcessSignal.sigabrt);
if(instance.launcherProcess != -1){
Process.killPid(instance.launcherProcess, ProcessSignal.sigabrt);
}
if(instance.eacProcess != -1){
Process.killPid(instance.eacProcess, ProcessSignal.sigabrt);
}
await supabase.from('hosts')
.delete()
.match({'id': instance.uuid});
exit(0);
}
}
class _GameInstance {
final String uuid;
final int gameProcess;
final int launcherProcess;
final int eacProcess;
_GameInstance(this.uuid, this.gameProcess, this.launcherProcess, this.eacProcess);
@override
String toString() {
return '{uuid: $uuid, gameProcess: $gameProcess, launcherProcess: $launcherProcess, eacProcess: $eacProcess}';
}
}

View File

@@ -1,6 +1,6 @@
name: reboot_launcher
description: Launcher for project reboot
version: "7.0.0"
version: "8.0.0"
publish_to: 'none'
@@ -13,7 +13,7 @@ dependencies:
bitsdojo_window:
path: ./dependencies/bitsdojo_window-0.1.5
fluent_ui: ^4.1.3
fluent_ui: ^4.6.2
bitsdojo_window_windows: ^0.1.5
system_theme: ^2.0.0
http: ^0.13.5
@@ -40,7 +40,8 @@ dependencies:
jaguar: ^3.1.3
hex: ^0.2.0
uuid: ^3.0.6
unrar_file: ^1.1.0
supabase_flutter: ^1.10.0
supabase: ^1.9.1
dev_dependencies:
flutter_test:
@@ -48,11 +49,13 @@ dev_dependencies:
flutter_lints: ^2.0.1
msix: ^3.6.3
flutter_distributor: ^0.3.4
flutter:
uses-material-design: true
assets:
- assets/builds/
- assets/browse/
- assets/dlls/
- assets/icons/
- assets/images/
@@ -67,7 +70,7 @@ msix_config:
display_name: Reboot Launcher
publisher_display_name: Auties00
identity_name: 31868Auties00.RebootLauncher
msix_version: 7.0.0.0
msix_version: 8.0.0.0
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
logo_path: ./assets/icons/reboot.ico
architecture: x64

View File

@@ -1,3 +1,4 @@
dart compile exe ./lib/watch.dart --output ./assets/browse/watch.exe
flutter_distributor package --platform windows --targets exe
flutter pub run msix:create
dart compile exe ./lib/cli.dart --output ./dist/cli/reboot.exe

View File

@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <system_theme/system_theme_plugin.h>
@@ -13,6 +14,8 @@
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
app_links
bitsdojo_window_windows
screen_retriever
system_theme

View File

@@ -20,6 +20,13 @@ add_executable(${BINARY_NAME} WIN32
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")

View File

@@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
// Version
//
#ifdef FLUTTER_BUILD_NUMBER
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#ifdef FLUTTER_BUILD_NAME
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif