This commit is contained in:
Alessandro Autiero
2025-03-08 17:06:01 +01:00
parent b319479def
commit 90448eeaa1
22 changed files with 589 additions and 670 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -128,7 +128,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",
"addVersion": "Add version",
"addVersion": "New version",
"downloadBuildName": "Download any version from the cloud",
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
"downloadBuildContent": "Download build",

View File

@@ -35,10 +35,8 @@ class BackendController extends GetxController {
late final RxBool started;
late final RxBool detached;
late final List<InfoBarEntry> _infoBars;
StreamSubscription? worker;
int? embeddedProcessPid;
HttpServer? localServer;
HttpServer? remoteServer;
StreamSubscription? _worker;
ServerImplementation? _implementation;
BackendController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
@@ -48,11 +46,6 @@ class BackendController extends GetxController {
host.text = _readHost();
port.text = _readPort();
_storage?.write("type", value.index);
if (!started.value) {
return;
}
stop();
});
host = TextEditingController(text: _readHost());
host.addListener(() =>
@@ -148,18 +141,27 @@ class BackendController extends GetxController {
detached.value = false;
}
Future<bool> toggleInteractive() async {
Future<bool> toggle() {
if(started.value) {
return stop(interactive: true);
}else {
return start(interactive: true);
}
}
Future<bool> start({required bool interactive}) async {
if(started.value) {
return true;
}
_cancel();
final stream = started.value ? stop() : start(
onExit: () {
_cancel();
_showRebootInfoBar(
translations.backendProcessError,
severity: InfoBarSeverity.error
);
},
final stream = startBackend(
type: type.value,
host: host.text,
port: port.text,
detached: detached.value,
onError: (errorMessage) {
_cancel();
stop(interactive: false);
_showRebootInfoBar(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
@@ -173,265 +175,203 @@ class BackendController extends GetxController {
);
final completer = Completer<bool>();
InfoBarEntry? entry;
worker = stream.listen((event) {
_worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event);
entry = _handeEvent(event, interactive);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
completer.complete(true);
}
});
return await completer.future;
}
Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) async* {
try {
if(started.value) {
return;
}
final serverType = type.value;
final hostData = this.host.text.trim();
final portData = this.port.text.trim();
started.value = true;
if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting);
}
if (hostData.isEmpty) {
yield ServerResult(ServerResultType.missingHostError);
started.value = false;
return;
}
if (portData.isEmpty) {
yield ServerResult(ServerResultType.missingPortError);
started.value = false;
return;
}
final portNumber = int.tryParse(portData);
if (portNumber == null) {
yield ServerResult(ServerResultType.illegalPortError);
started.value = false;
return;
}
if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
yield ServerResult(ServerResultType.freeingPort);
final result = await freeBackendPort();
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
if(!result) {
started.value = false;
return;
}
}
switch(serverType){
case ServerType.embedded:
final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) {
if(started.value) {
started.value = false;
onError(errorMessage);
}
});
watchProcess(process.pid).then((_) {
if(started.value) {
started.value = false;
onExit();
}
});
embeddedProcessPid = process.pid;
break;
case ServerType.remote:
yield ServerResult(ServerResultType.pingingRemote);
final uriResult = await pingBackend(hostData, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
started.value = false;
return;
}
remoteServer = await startRemoteBackendProxy(uriResult);
break;
case ServerType.local:
if(portNumber != kDefaultBackendPort) {
yield ServerResult(ServerResultType.pingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
started.value = false;
return;
}
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
}else {
// If the local server is running on port 3551 there is no reverse proxy running
// We only need to check if everything is working
started.value = false;
}
break;
}
yield ServerResult(ServerResultType.pingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
remoteServer?.close(force: true);
localServer?.close(force: true);
started.value = false;
return;
}
yield ServerResult(ServerResultType.startSuccess);
}catch(error, stackTrace) {
yield ServerResult(
ServerResultType.startError,
error: error,
stackTrace: stackTrace
);
remoteServer?.close(force: true);
localServer?.close(force: true);
started.value = false;
}
}
Stream<ServerResult> stop() async* {
Future<bool> stop({required bool interactive}) async {
if(!started.value) {
return;
return true;
}
yield ServerResult(ServerResultType.stopping);
started.value = false;
try{
switch(type()){
case ServerType.embedded:
final embeddedProcessPid = this.embeddedProcessPid;
if(embeddedProcessPid != null) {
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm);
this.embeddedProcessPid = null;
}
break;
case ServerType.remote:
await remoteServer?.close(force: true);
remoteServer = null;
break;
case ServerType.local:
await localServer?.close(force: true);
localServer = null;
break;
_cancel();
final stream = stopBackend(
type: type.value,
implementation: _implementation
);
final completer = Completer<bool>();
InfoBarEntry? entry;
_worker = stream.listen((event) {
entry?.close();
entry = _handeEvent(event, interactive);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
completer.complete(true);
}
yield ServerResult(ServerResultType.stopSuccess);
}catch(error, stackTrace){
yield ServerResult(
ServerResultType.stopError,
error: error,
stackTrace: stackTrace
);
started.value = true;
}
});
return await completer.future;
}
void _cancel() {
worker?.cancel(); // Do not await or it will hang
_worker?.cancel(); // Do not await or it will hang
_infoBars.forEach((infoBar) => infoBar.close());
_infoBars.clear();
}
InfoBarEntry _handeEvent(ServerResult event) {
log("[BACKEND] Handling event: $event");
InfoBarEntry? _handeEvent(ServerResult event, bool interactive) {
log("[BACKEND] Handling event: $event (interactive: $interactive, start: ${event.type.isStart}, error: ${event.type.isError})");
started.value = event.type.isStart && !event.type.isError;
switch (event.type) {
case ServerResultType.starting:
return _showRebootInfoBar(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
if(interactive) {
return _showRebootInfoBar(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startSuccess:
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success
);
if(interactive) {
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.startError:
return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
if(interactive) {
return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.stopping:
return _showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
if(interactive) {
return _showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.stopSuccess:
return _showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
if(interactive) {
return _showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
}else {
return null;
}
case ServerResultType.stopError:
return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.missingHostError:
return _showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
case ServerResultType.missingPortError:
return _showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.illegalPortError:
return _showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.freeingPort:
return _showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
case ServerResultType.freePortSuccess:
return _showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
case ServerResultType.freePortError:
return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.pingingRemote:
return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingingLocal:
return _showRebootInfoBar(
translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingError:
return _showRebootInfoBar(
translations.pingError(type.value.name),
severity: InfoBarSeverity.error
);
if(interactive) {
return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.startMissingHostError:
if(interactive) {
return _showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startMissingPortError:
if(interactive) {
return _showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startIllegalPortError:
if(interactive) {
return _showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startFreeingPort:
if(interactive) {
return _showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startFreePortSuccess:
if(interactive) {
return _showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}else {
return null;
}
case ServerResultType.startFreePortError:
if(interactive) {
return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
}else {
return null;
}
case ServerResultType.startPingingRemote:
if(interactive) {
return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startPingingLocal:
if(interactive) {
return _showRebootInfoBar(
translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
}else {
return null;
}
case ServerResultType.startPingError:
if(interactive) {
return _showRebootInfoBar(
translations.pingError(type.value.name),
severity: InfoBarSeverity.error
);
}else {
return null;
}
case ServerResultType.startedImplementation:
_implementation = event.implementation;
return null;
}
}
@@ -597,4 +537,11 @@ class BackendController extends GetxController {
}
return result;
}
Future<void> restart() async {
if(started.value) {
await stop(interactive: false);
await start(interactive: true);
}
}
}

View File

@@ -27,7 +27,6 @@ class DllController extends GetxController {
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
InfoBarEntry? infoBarEntry;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
@@ -75,6 +74,7 @@ class DllController extends GetxController {
}
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
InfoBarEntry? infoBarEntry;
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
@@ -100,8 +100,8 @@ class DllController extends GetxController {
}
await Future.wait(
[
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text),
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text, false),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text, true),
Future.delayed(const Duration(seconds: 1))
],
eagerError: false

View File

@@ -110,7 +110,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
final backendResult = _backendController.started() || await _backendController.toggleInteractive();
final backendResult = _backendController.started() || await _backendController.toggle();
if(!backendResult){
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop(
@@ -526,7 +526,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
await _operation?.cancel();
_operation = null;
_backendController.stop();
_backendController.stop(interactive: false);
}
host = host ?? widget.host;
@@ -629,7 +629,7 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.tokenError:
_backendController.stop();
_backendController.stop(interactive: false);
showRebootInfoBar(
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,

View File

@@ -162,7 +162,12 @@ class _BackendPageState extends RebootPageState<BackendPage> {
key: backendDetachedOverlayTargetKey,
child: ToggleSwitch(
checked: _backendController.detached(),
onChanged: (value) => _backendController.detached.value = value
onChanged: (value) async {
_backendController.detached.value = value;
if(_backendController.started.value) {
await _backendController.restart();
}
}
),
),
],

View File

@@ -75,6 +75,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
lastPage = index;
_pageController.jumpToPage(index);
pagesController.add(null);
});
}
@@ -152,7 +153,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
try {
if(_backendController.started.value) {
await _backendController.toggleInteractive();
await _backendController.toggle();
}
}catch(error) {
log("[BACKEND] Cannot stop backend on exit: $error");
@@ -524,36 +525,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
);
}
Widget get _backButton => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0
)),
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.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),
)
);
Widget get _autoSuggestBox => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,

View File

@@ -1,3 +1,6 @@
import 'dart:math';
import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
@@ -36,6 +39,7 @@ class SettingsPage extends RebootPage {
class _SettingsPageState extends RebootPageState<SettingsPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
int? _downloadFromMirrorId;
@override
Widget? get button => null;
@@ -115,7 +119,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
}
_dllController.customGameServer.value = entry.key;
_dllController.infoBarEntry?.close();
if(!entry.key) {
_dllController.updateGameServerDll(
force: true
@@ -141,11 +144,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
@@ -194,6 +193,24 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
}
});
void _scheduleMirrorDownload(String value) async {
if(_downloadFromMirrorId != null) {
return;
}
if(Uri.tryParse(value) == null) {
return;
}
final id = Random.secure().nextInt(1000000);
_downloadFromMirrorId = id;
await Future.delayed(const Duration(seconds: 2));
if(_downloadFromMirrorId == id) {
await _dllController.updateGameServerDll(force: true);
}
_downloadFromMirrorId = null;
}
Widget get _internalFilesNewServerSource => Obx(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
@@ -209,11 +226,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
@@ -273,7 +286,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
text: Text(entry.text),
onPressed: () {
_dllController.timer.value = entry;
_dllController.infoBarEntry?.close();
_dllController.updateGameServerDll(
force: true
);

View File

@@ -45,7 +45,7 @@ class _ServerButtonState extends State<ServerButton> {
builder: (context, snapshot) => Obx(() => Text(_buttonText))
),
),
onPressed: () => _controller.toggleInteractive()
onPressed: () => _controller.toggle()
)
)
);

View File

@@ -32,18 +32,16 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
));
}
MenuFlyoutItem _createItem(ServerType type) {
return MenuFlyoutItem(
text: Text(type.label),
onPressed: () async {
_controller.stop();
_controller.type.value = type;
}
);
}
MenuFlyoutItem _createItem(ServerType type) => MenuFlyoutItem(
text: Text(type.label),
onPressed: () async {
await _controller.stop(interactive: false);
_controller.type.value = type;
}
);
}
extension ServerTypeExtension on ServerType {
extension _ServerTypeExtension on ServerType {
String get label {
return this == ServerType.embedded ? translations.embedded
: this == ServerType.remote ? translations.remote

View File

@@ -1,6 +1,6 @@
name: reboot_launcher
description: Graphical User Interface for Project Reboot
version: "10.0.6"
version: "10.0.7"
publish_to: 'none'
@@ -43,6 +43,7 @@ dependencies:
# Async helpers
async: ^2.11.0
sync: ^0.3.0
synchronized: ^3.3.0+3
# State management
get: ^4.6.6