Fixed some small things

This commit is contained in:
Alessandro Autiero
2022-10-16 02:08:01 +02:00
parent 968739de9e
commit 699367200f
147 changed files with 953 additions and 54005 deletions

View File

@@ -6,8 +6,10 @@ import 'package:args/args.dart';
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/node.dart';
import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server_standalone.dart';
@@ -23,9 +25,10 @@ import 'package:http/http.dart' as http;
// Needed because binaries can't be loaded in any other way
const String _craniumDownload = "https://filebin.net/ybn0gme7dqjr4zup/cranium.dll";
const String _consoleDownload = "https://filebin.net/ybn0gme7dqjr4zup/console.dll";
const String _injectorDownload = "https://filebin.net/ybn0gme7dqjr4zup/injector.exe";
Process? _gameProcess;
Process? _eacProcess;
Process? _launcherProcess;
void main(List<String> args){
handleCLI(args);
@@ -73,9 +76,11 @@ Future<String?> _getWindowsPath(String folderID) {
}
Future<void> handleCLI(List<String> args) async {
stdout.writeln("Reboot Launcher CLI Tool");
stdout.writeln("Reboot Launcher");
stdout.writeln("Wrote by Auties00");
stdout.writeln("Version 3.11");
stdout.writeln("Version 3.13");
_killOld();
var gameJson = await _getControllerJson("game");
var serverJson = await _getControllerJson("server");
@@ -86,11 +91,11 @@ Future<void> handleCLI(List<String> args) async {
..addCommand("launch")
..addOption("version", defaultsTo: gameJson["version"])
..addOption("username")
..addOption("server-type", allowed: ["embedded", "remote"], defaultsTo: serverJson["embedded"] ?? true ? "embedded" : "remote")
..addOption("server-host", defaultsTo: serverJson["host"])
..addOption("server-port", defaultsTo: serverJson["port"])
..addOption("server-type", allowed: _getServerTypes(), defaultsTo: _getDefaultServerType(serverJson))
..addOption("server-host")
..addOption("server-port")
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path)
..addOption("type", allowed: ["client", "server", "headless_server"], defaultsTo: _getDefaultType(gameJson))
..addOption("type", allowed: _getTypes(), defaultsTo: _getDefaultType(gameJson))
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
..addFlag("log", defaultsTo: false);
var result = parser.parse(args);
@@ -101,9 +106,11 @@ Future<void> handleCLI(List<String> args) async {
return;
}
var dll = result["dll"];
var type = _getType(result);
var username = result["username"];
username ??= gameJson["${type == GameType.client ? "game" : "server"}_username"];
var verbose = result["log"];
var dummyVersion = _createVersion(gameJson["version"], result["version"], versions);
await _updateDLLs();
@@ -123,31 +130,57 @@ Future<void> handleCLI(List<String> args) async {
await patchExe(dummyVersion.executable!);
}
var started = await _startServerIfNeeded(result);
var serverType = _getServerType(result);
var host = result["server-host"] ?? serverJson["${serverType.id}_host"];
var port = result["server-port"] ?? serverJson["${serverType.id}_port"];
var started = await _startServerIfNeeded(host, port, serverType);
if(!started){
stderr.writeln("Cannot start server!");
return;
}
await _startGameProcess(dummyVersion, result["dll"], type != GameType.client, result);
await _injectOrShowError("cranium.dll");
await _startGameProcess(username, type, verbose, dll, dummyVersion);
}
void _killOld() async {
var shell = Shell(
commandVerbose: false,
commentVerbose: false,
verbose: false
);
try {
await shell.run("taskkill /f /im FortniteLauncher.exe");
await shell.run("taskkill /f /im FortniteClient-Win64-Shipping_EAC.exe");
}catch(_){
}
}
Iterable<String> _getTypes() => GameType.values.map((entry) => entry.id);
Iterable<String> _getServerTypes() => ServerType.values.map((entry) => entry.id);
String _getDefaultServerType(Map<String, dynamic> json) {
var type = ServerType.values.elementAt(json["type"] ?? 0);
return type.id;
}
GameType _getType(ArgResults result) {
var type = result["type"];
switch(type){
case "client":
return GameType.client;
case "server":
return GameType.server;
case "headless_server":
return GameType.headlessServer;
default:
throw Exception("Unknown game type: $result. Use --type only with client, server or headless_server");
var type = GameType.of(result["type"]);
if(type == null){
throw Exception("Unknown game type: $result. Use --type only with ${_getTypes().join(", ")}");
}
return type;
}
ServerType _getServerType(ArgResults result) {
var type = ServerType.of(result["server-type"]);
if(type == null){
throw Exception("Unknown server type: $result. Use --server-type only with ${_getServerTypes().join(", ")}");
}
return type;
}
String _getDefaultType(Map<String, dynamic> json){
@@ -183,16 +216,6 @@ Future<void> _updateDLLs() async {
await craniumDll.writeAsBytes(response.bodyBytes);
}
var injectorExe = await loadBinary("injector.exe", true);
if(!injectorExe.existsSync()){
var response = await http.get(Uri.parse(_injectorDownload));
if(response.statusCode != 200){
throw Exception("Cannot download injector");
}
await injectorExe.writeAsBytes(response.bodyBytes);
}
}
List<FortniteVersion> _getVersions(Map<String, dynamic> gameJson) {
@@ -201,26 +224,27 @@ List<FortniteVersion> _getVersions(Map<String, dynamic> gameJson) {
.toList();
}
Future<void> _startGameProcess(FortniteVersion dummyVersion, String rebootDll, bool host, ArgResults result) async {
var gamePath = dummyVersion.executable?.path;
Future<void> _startGameProcess(String? username, GameType type, bool verbose, String dll, FortniteVersion version) async {
var gamePath = version.executable?.path;
if (gamePath == null) {
throw Exception("${dummyVersion.location
throw Exception("${version.location
.path} no longer contains a Fortnite executable. Did you delete it?");
}
var username = result["username"];
var hosting = type != GameType.client;
if (username == null) {
username = "Reboot${host ? 'Host' : 'Player'}";
username = "Reboot${hosting ? 'Host' : 'Player'}";
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
}
var verbose = result["log"];
_gameProcess = await Process.start(gamePath, createRebootArgs(username, result["type"] == "headless_server"))
_gameProcess = await Process.start(gamePath, createRebootArgs(username, type == GameType.headlessServer))
..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, rebootDll, host, verbose));
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
await _injectOrShowError("cranium.dll");
}
void _onClose() {
_kill();
stdout.writeln("The game was closed");
exit(0);
}
@@ -230,8 +254,8 @@ Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
return;
}
var process = await Process.start(dummyVersion.eacExecutable!.path, []);
Win32Process(process.pid).suspend();
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
Win32Process(_eacProcess!.pid).suspend();
}
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
@@ -239,34 +263,50 @@ Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
return;
}
var process = await Process.start(dummyVersion.launcher!.path, []);
Win32Process(process.pid).suspend();
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
Win32Process(_launcherProcess!.pid).suspend();
}
Future<bool> _startServerIfNeeded(ArgResults result) async {
Future<bool> _startServerIfNeeded(String? host, String? port, ServerType type) async {
stdout.writeln("Starting lawin server...");
if (!await isLawinPortFree()) {
stdout.writeln("A lawin server is already active");
return true;
}
switch(type){
case ServerType.local:
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
if(result == null){
throw Exception("Local lawin server is not running");
}
if (result["server-type"] == "embedded") {
stdout.writeln("Starting an embedded server...");
return await _changeEmbeddedServerState();
}
stdout.writeln("Detected local lawin server");
return true;
case ServerType.embedded:
stdout.writeln("Starting an embedded server...");
return await _changeEmbeddedServerState();
case ServerType.remote:
if(host == null){
throw Exception("Missing host for remote server");
}
var host = result["server-host"];
var port = result["server-port"];
stdout.writeln("Starting a reverse proxy to $host:$port");
return await _changeReverseProxyState(host, port) != null;
if(port == null){
throw Exception("Missing host for remote server");
}
stdout.writeln("Starting a reverse proxy to $host:$port");
return await _changeReverseProxyState(host, port) != null;
}
}
Future<bool> _changeEmbeddedServerState() async {
var nodeProcess = await Process.run("where", ["node"]);
if(nodeProcess.exitCode != 0) {
var node = await hasNode();
if(!node) {
throw Exception("Missing node, cannot start embedded server");
}
var free = await isLawinPortFree();
if(!free){
stdout.writeln("Server is already running on port 3551");
return true;
}
if(!serverLocation.existsSync()) {
await downloadServer(false);
}
@@ -331,27 +371,33 @@ FortniteVersion _createVersion(String? versionName, String? versionPath, List<Fo
"Specify a version using --version or open the launcher GUI and select it manually");
}
void _onGameOutput(String line, String rebootDll, bool host, bool verbose) {
if(verbose) {
stdout.writeln(line);
}
if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) {
_onClose();
return;
}
if(line.contains("port 3551 failed: Connection refused")){
stderr.writeln("Connection refused from lawin server");
_onClose();
return;
}
if(line.contains("HTTP 400 response from ")){
if(line.contains("HTTP 400 response from ") || line.contains("Unable to login to Fortnite servers")){
stderr.writeln("Connection refused from lawin server");
_onClose();
return;
}
if(line.contains("Network failure when attempting to check platform restrictions")){
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
stderr.writeln("Expired token, please reopen the game");
_kill();
_onClose();
return;
}
@@ -365,6 +411,12 @@ void _onGameOutput(String line, String rebootDll, bool host, bool verbose) {
}
}
void _kill() {
_gameProcess?.kill(ProcessSignal.sigabrt);
_eacProcess?.kill(ProcessSignal.sigabrt);
_launcherProcess?.kill(ProcessSignal.sigabrt);
}
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
if (_gameProcess == null) {
return;
@@ -377,18 +429,8 @@ Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
throw Exception("Cannot inject $dll: missing file");
}
var success = await injectDll(_gameProcess!.pid, dll.path, true);
if (success) {
return;
}
_onInjectError(binary);
await injectDll(_gameProcess!.pid, dll.path);
} catch (exception) {
_onInjectError(binary);
throw Exception("Cannot inject binary: $binary");
}
}
void _onInjectError(String binary) {
stderr.writeln(injectLogFile.readAsStringSync());
throw Exception("Cannot inject binary: $binary");
}

View File

@@ -4,35 +4,35 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/server.dart';
import '../util/server_standalone.dart';
import '../model/server_type.dart';
class ServerController extends GetxController {
static const String _serverName = "127.0.0.1";
static const String _serverPort = "3551";
late final GetStorage _storage;
late final TextEditingController host;
late final TextEditingController port;
late final RxBool embedded;
late final Rx<ServerType> type;
late final RxBool warning;
late RxBool started;
HttpServer? reverseProxy;
ServerController() {
var storage = GetStorage("server");
host = TextEditingController(text: storage.read("host") ?? "");
host.addListener(() => storage.write("host", host.text));
_storage = GetStorage("server");
port = TextEditingController(text: storage.read("port") ?? "");
port.addListener(() => storage.write("port", port.text));
embedded = RxBool(storage.read("embedded") ?? true);
embedded.listen((value) {
storage.write("embedded", value);
type = Rx(ServerType.values.elementAt(_storage.read("type") ?? 0));
type.listen((value) {
host.text = _readHost();
port.text = _readPort();
_storage.write("type", value.index);
if(!started.value) {
return;
}
if(value){
if(value == ServerType.remote){
reverseProxy?.close(force: true);
reverseProxy = null;
started(false);
@@ -44,11 +44,25 @@ class ServerController extends GetxController {
.then((value) => started(false));
});
warning = RxBool(storage.read("lawin_value") ?? true);
warning.listen((value) => storage.write("lawin_value", value));
host = TextEditingController(text: _readHost());
host.addListener(() => _storage.write("${type.value.id}_host", host.text));
port = TextEditingController(text: _readPort());
port.addListener(() => _storage.write("${type.value.id}_port", port.text));
warning = RxBool(_storage.read("lawin_value") ?? true);
warning.listen((value) => _storage.write("lawin_value", value));
started = RxBool(false);
isLawinPortFree()
.then((value) => !embedded.value ? {} : started = RxBool(!value));
}
String _readHost() {
String? value = _storage.read("${type.value.id}_host");
return value != null && value.isNotEmpty ? value
: type.value != ServerType.remote ? _serverName : "";
}
String _readPort() {
return _storage.read("${type.value.id}_port") ?? _serverPort;
}
}

View File

@@ -19,22 +19,20 @@ class SettingsController extends GetxController {
SettingsController() {
_storage = GetStorage("settings");
rebootDll = _createController("reboot", "reboot.dll");
consoleDll = _createController("console", "console.dll");
craniumDll = _createController("cranium", "cranium.dll");
autoUpdate = RxBool(_storage.read("auto_update") ?? true);
autoUpdate.listen((value) => _storage.write("auto_update", value));
}
TextEditingController _createController(String key, String name) {
var controller = TextEditingController(text: _storage.read(key) ?? "$safeBinariesDirectory\\$name");
controller.addListener(() {
if(controller.text.isEmpty || !File(controller.text).existsSync()) {
return;
}
loadBinary(name, true);
_storage.write(key, controller.text);
});
var controller = TextEditingController(text: _storage.read(key) ?? "$safeBinariesDirectory\\$name");
controller.addListener(() => _storage.write(key, controller.text));
return controller;
}

View File

@@ -1,6 +1,5 @@
import 'dart:io';
import 'package:path/path.dart' as path;
class FortniteVersion {

View File

@@ -1,5 +1,32 @@
enum GameType {
client,
server,
headlessServer
headlessServer;
static GameType? of(String id){
try {
return GameType.values
.firstWhere((element) => element.id == id);
}catch(_){
return null;
}
}
String get id {
return this == GameType.client ? "client"
: this == GameType.server ? "server"
: "headless_server";
}
String get name {
return this == GameType.client ? "Client"
: this == GameType.server ? "Server"
: "Headless Server";
}
String get message {
return this == GameType.client ? "A fortnite client will be launched to play multiplayer games"
: this == GameType.server ? "A fortnite client will be launched to host multiplayer games"
: "A fortnite client will be launched in the background to host multiplayer games";
}
}

View File

@@ -0,0 +1,32 @@
enum ServerType {
embedded,
remote,
local;
static ServerType? of(String id){
try {
return ServerType.values
.firstWhere((element) => element.id == id);
}catch(_){
return null;
}
}
String get id {
return this == ServerType.embedded ? "embedded"
: this == ServerType.remote ? "remote"
: "local";
}
String get name {
return this == ServerType.embedded ? "Embedded"
: this == ServerType.remote ? "Remote"
: "Local";
}
String get message {
return this == ServerType.embedded ? "A server will be automatically started in the background"
: this == ServerType.remote ? "A reverse proxy to the remote server will be created"
: "Assumes that you are running yourself the server locally";
}
}

View File

@@ -1,4 +1,5 @@
import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/page/settings_page.dart';
import 'package:reboot_launcher/src/page/launcher_page.dart';
@@ -16,7 +17,13 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State<HomePage> with WindowListener {
static const double _headerSize = 48.0;
static const double _sectionSize = 97.0;
static const int _headerButtonCount = 3;
static const int _sectionButtonCount = 3;
bool _focused = true;
bool _shouldMaximize = false;
int _index = 0;
@override
@@ -46,34 +53,76 @@ class _HomePageState extends State<HomePage> with WindowListener {
return Stack(
children: [
NavigationView(
pane: NavigationPane(
selected: _index,
onChanged: (index) => setState(() => _index = index),
displayMode: PaneDisplayMode.top,
indicator: const EndNavigationIndicator(),
items: [
_createPane("Home", FluentIcons.game),
_createPane("Lawin", FluentIcons.server_enviroment),
_createPane("Settings", FluentIcons.settings)
],
trailing: WindowTitleBar(focused: _focused)),
content: NavigationBody(
index: _index,
children: [
const LauncherPage(),
ServerPage(),
SettingsPage()
]
)
pane: NavigationPane(
size: const NavigationPaneSize(
topHeight: _headerSize
),
selected: _index,
onChanged: (index) => setState(() => _index = index),
displayMode: PaneDisplayMode.top,
indicator: const EndNavigationIndicator(),
items: [
PaneItem(
title: const Text("Home"),
icon: const Icon(FluentIcons.game),
body: const LauncherPage()
),
PaneItem(
title: const Text("Lawin"),
icon: const Icon(FluentIcons.server_enviroment),
body: ServerPage()
),
PaneItem(
title: const Text("Settings"),
icon: const Icon(FluentIcons.settings),
body: SettingsPage()
)
]
),
),
_createTitleBar(),
_createGestureHandler(),
if(_focused && isWin11)
const WindowBorder()
],
);
}
PaneItem _createPane(String label, IconData icon) {
return PaneItem(icon: Icon(icon), title: Text(label));
Align _createTitleBar() {
return Align(
alignment: Alignment.topRight,
child: WindowTitleBar(focused: _focused),
);
}
// Hacky way to get it to work while having maximum performance and no modifications to external libs
Padding _createGestureHandler() {
return Padding(
padding: const EdgeInsets.only(
left: _sectionSize * _headerButtonCount,
right: _headerSize * _sectionButtonCount,
),
child: SizedBox(
height: _headerSize,
child: GestureDetector(
onDoubleTap: () {
if(!_shouldMaximize){
return;
}
appWindow.maximizeOrRestore();
_shouldMaximize = false;
},
onDoubleTapDown: (details) => _shouldMaximize = true,
onHorizontalDragStart: (event) => appWindow.startDragging(),
onVerticalDragStart: (event) => appWindow.startDragging()
),
),
);
}
}

View File

@@ -75,38 +75,41 @@ class _LauncherPageState extends State<LauncherPage> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _gameController.updater,
builder: (context, snapshot) {
if (!snapshot.hasData && !snapshot.hasError) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
return Padding(
padding: const EdgeInsets.all(12.0),
child: FutureBuilder(
future: _gameController.updater,
builder: (context, snapshot) {
if (!snapshot.hasData && !snapshot.hasError) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
],
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
if(snapshot.hasError)
_createUpdateError(snapshot),
UsernameBox(),
const VersionSelector(),
DeploymentSelector(),
const LaunchButton()
],
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(snapshot.hasError)
_createUpdateError(snapshot),
UsernameBox(),
const VersionSelector(),
const DeploymentSelector(),
const LaunchButton()
],
);
}
),
);
}

View File

@@ -14,21 +14,24 @@ class ServerPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Obx(() => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(_serverController.warning.value)
WarningInfo(
text: "The lawin server handles authentication and parties, not game hosting",
icon: FluentIcons.accept,
onPressed: () => _serverController.warning.value = false
),
HostInput(),
PortInput(),
LocalServerSwitch(),
ServerButton()
]
));
return Padding(
padding: const EdgeInsets.all(12.0),
child: Obx(() => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(_serverController.warning.value)
WarningInfo(
text: "The lawin server handles authentication and parties, not game hosting",
icon: FluentIcons.accept,
onPressed: () => _serverController.warning.value = false
),
HostInput(),
PortInput(),
LocalServerSwitch(),
ServerButton()
]
)),
);
}
}

View File

@@ -14,11 +14,11 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: [
Form(
autovalidateMode: AutovalidateMode.always,
child: Column(
return Padding(
padding: const EdgeInsets.all(12.0),
child: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -29,7 +29,8 @@ class SettingsPage extends StatelessWidget {
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: _checkDll
validator: _checkDll,
validatorMode: AutovalidateMode.always
),
FileSelector(
@@ -39,7 +40,8 @@ class SettingsPage extends StatelessWidget {
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: _checkDll
validator: _checkDll,
validatorMode: AutovalidateMode.always
),
FileSelector(
@@ -49,22 +51,23 @@ class SettingsPage extends StatelessWidget {
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: _checkDll
validator: _checkDll,
validatorMode: AutovalidateMode.always
),
SmartSwitch(
value: _settingsController.autoUpdate,
label: "Update DLLs"
),
)
],
)
),
),
const Align(
alignment: Alignment.bottomRight,
child: Text("Version 3.11${kDebugMode ? '-DEBUG' : ''}")
)
],
const Align(
alignment: Alignment.bottomRight,
child: Text("Version 3.13${kDebugMode ? '-DEBUG' : ''}")
)
],
),
);
}

View File

@@ -117,7 +117,11 @@ Future<void> downloadArchiveBuild(String archiveUrl, String destination,
var output = Directory(destination);
await output.create(recursive: true);
var shell = Shell(workingDirectory: internalBinariesDirectory);
var shell = Shell(
commandVerbose: false,
commentVerbose: false,
workingDirectory: internalBinariesDirectory
);
await shell.run("./winrar.exe x ${tempFile.path} *.* \"${output.path}\"");
} finally {
if (await tempFile.exists()) {

View File

@@ -1,23 +1,91 @@
import 'dart:io';
// ignore_for_file: non_constant_identifier_names
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'dart:ffi';
File injectLogFile = File("${Platform.environment["Temp"]}/server.txt");
import 'package:win32/win32.dart';
import 'package:ffi/ffi.dart';
// This can be done easily with win32 apis but for some reason it doesn't work on all machines
// Update: it was a missing permission error, it could be refactored now
Future<bool> injectDll(int pid, String dll, [bool useSafeBinariesHome = false]) async {
var shell = Shell(
commandVerbose: false,
commentVerbose: false,
workingDirectory: useSafeBinariesHome ? safeBinariesDirectory : internalBinariesDirectory
final _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreateRemoteThread = _kernel32.lookupFunction<
IntPtr Function(
IntPtr hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
IntPtr dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
Uint32 dwCreationFlags,
Pointer<Uint32> lpThreadId),
int Function(
int hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
int dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
int dwCreationFlags,
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
int CreateRemoteThread(
int hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
int dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
int dwCreationFlags,
Pointer<Uint32> lpThreadId) =>
_CreateRemoteThread(hProcess, lpThreadAttributes, dwStackSize,
loadLibraryAddress, lpParameter, dwCreationFlags, lpThreadId);
Future<void> injectDll(int pid, String dll) async {
var process = OpenProcess(
0x43A,
0,
pid
);
var process = await shell.run("./injector.exe -p $pid --inject \"$dll\"");
var success = process.outText.contains("Successfully injected module");
if (!success) {
injectLogFile.writeAsString(process.outText);
var processAddress = GetProcAddress(
GetModuleHandle("KERNEL32".toNativeUtf16()),
"LoadLibraryA".toNativeUtf8()
);
if (processAddress == nullptr) {
throw Exception("Cannot get process address for pid $pid");
}
return success;
var dllAddress = VirtualAllocEx(
process,
nullptr,
dll.length + 1,
0x3000,
0x4
);
var writeMemoryResult = WriteProcessMemory(
process,
dllAddress,
dll.toNativeUtf8(),
dll.length,
nullptr
);
if (writeMemoryResult != 1) {
throw Exception("Memory write failed");
}
var createThreadResult = CreateRemoteThread(
process,
nullptr,
0,
processAddress,
dllAddress,
0,
nullptr
);
if (createThreadResult == -1) {
throw Exception("Thread creation failed");
}
var closeResult = CloseHandle(process);
if(closeResult != 1){
throw Exception("Cannot close handle");
}
}

24
lib/src/util/node.dart Normal file
View File

@@ -0,0 +1,24 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'binary.dart';
import 'package:http/http.dart' as http;
const String _nodeUrl = "https://nodejs.org/dist/v18.11.0/node-v18.11.0-win-x86.zip";
File get embeddedNode =>
File("$safeBinariesDirectory/node-v18.11.0-win-x86/node.exe");
Future<bool> hasNode() async {
var nodeProcess = await Process.run("where", ["node"]);
return nodeProcess.exitCode == 0;
}
Future<bool> downloadNode(ignored) async {
var response = await http.get(Uri.parse(_nodeUrl));
var tempZip = File("${tempDirectory.path}/nodejs.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, safeBinariesDirectory);
return true;
}

View File

@@ -1,14 +1,40 @@
import 'dart:io';
import 'dart:math';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/node.dart';
import 'package:reboot_launcher/src/util/server_standalone.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_proxy/shelf_proxy.dart';
Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, String port, HttpServer? server) async {
Future<bool> checkLocalServer(BuildContext context, String host, String port, bool closeAutomatically) async {
host = host.trim();
if(host.isEmpty){
showSnackbar(
context, const Snackbar(content: Text("Missing host name")));
return false;
}
port = port.trim();
if(port.isEmpty){
showSnackbar(
context, const Snackbar(content: Text("Missing port", textAlign: TextAlign.center)));
return false;
}
if(int.tryParse(port) == null){
showSnackbar(
context, const Snackbar(content: Text("Invalid port, use only numbers", textAlign: TextAlign.center)));
return false;
}
return await _showCheck(context, host, port, false, closeAutomatically) != null;
}
Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, String port, bool closeAutomatically, HttpServer? server) async {
if(server != null){
try{
server.close(force: true);
@@ -40,7 +66,7 @@ Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, S
}
try{
var uri = await _showReverseProxyCheck(context, host, port);
var uri = await _showCheck(context, host, port, true, closeAutomatically);
if(uri == null){
return null;
}
@@ -52,8 +78,9 @@ Future<HttpServer?> changeReverseProxyState(BuildContext context, String host, S
}
}
Future<Uri?> _showReverseProxyCheck(BuildContext context, String host, String port) async {
Future<Uri?> _showCheck(BuildContext context, String host, String port, bool remote, bool closeAutomatically) async {
var future = ping(host, port);
Uri? result;
return await showDialog(
context: context,
builder: (context) => ContentDialog(
@@ -63,24 +90,38 @@ Future<Uri?> _showReverseProxyCheck(BuildContext context, String host, String po
if(snapshot.hasError){
return SizedBox(
width: double.infinity,
child: Text("Cannot ping remote server: ${snapshot.error}" , textAlign: TextAlign.center)
child: Text("Cannot ping ${remote ? "remote" : "local"} server: ${snapshot.error}" , textAlign: TextAlign.center)
);
}
if(snapshot.connectionState == ConnectionState.done && !snapshot.hasData){
return const SizedBox(
return SizedBox(
width: double.infinity,
child: Text("The remote server doesn't work correctly or the IP and/or the port are incorrect" , textAlign: TextAlign.center)
child: Text(
"The ${remote ? "remote" : "local"} server doesn't work correctly ${remote ? "or the IP and/or the port are incorrect" : ""}",
textAlign: TextAlign.center
)
);
}
result = snapshot.data;
if(snapshot.hasData){
Navigator.of(context).pop(snapshot.data);
if(remote || closeAutomatically) {
Navigator.of(context).pop(result);
}
return const SizedBox(
width: double.infinity,
child: Text(
"The server works correctly",
textAlign: TextAlign.center
)
);
}
return const InfoLabel(
label: "Pinging remote lawin server...",
child: SizedBox(
return InfoLabel(
label: "Pinging ${remote ? "remote" : "local"} lawin server...",
child: const SizedBox(
width: double.infinity,
child: ProgressBar()
)
@@ -91,7 +132,7 @@ Future<Uri?> _showReverseProxyCheck(BuildContext context, String host, String po
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(context).pop(result),
child: const Text('Close'),
))
]
@@ -148,39 +189,70 @@ Future<bool> changeEmbeddedServerState(BuildContext context, bool running) async
return false;
}
var nodeProcess = await Process.run("where", ["node"]);
if(nodeProcess.exitCode == 0) {
if(!(await serverLocation.exists()) && !(await _showServerDownloadInfo(context, false))){
var free = await isLawinPortFree();
if (!free) {
var shouldKill = await _showAlreadyBindPortWarning(context);
if (!shouldKill) {
return false;
}
var serverRunner = File("${serverLocation.path}/start.bat");
if (!(await serverRunner.exists())) {
_showEmbeddedError(context, serverRunner.path);
return false;
}
var nodeModules = Directory("${serverLocation.path}/node_modules");
if (!(await nodeModules.exists())) {
await Process.run("${serverLocation.path}/install_packages.bat", [],
workingDirectory: serverLocation.path);
}
await Process.start(serverRunner.path, [], workingDirectory: serverLocation.path);
return true;
var releaseBat = await loadBinary("release.bat", false);
await Process.run(releaseBat.path, []);
}
var portableServer = await loadBinary("LawinServer.exe", true);
if(!(await portableServer.exists()) && !(await _showServerDownloadInfo(context, true))){
var node = await hasNode();
var useLocalNode = false;
if(!node) {
useLocalNode = true;
if(!embeddedNode.existsSync()){
var result = await _showNodeDownloadInfo(context);
if(!result) {
return false;
}
}
}
if(!serverLocation.existsSync()) {
var result = await _showServerDownloadInfo(context);
if(!result){
return false;
}
}
var serverRunner = File("${serverLocation.path}/start.bat");
if (!serverRunner.existsSync()) {
_showEmbeddedError(context, "missing file ${serverRunner.path}");
return false;
}
await Process.start(portableServer.path, []);
return true;
var nodeModules = Directory("${serverLocation.path}/node_modules");
if (!nodeModules.existsSync()) {
await Process.run("${serverLocation.path}/install_packages.bat", [],
workingDirectory: serverLocation.path);
}
try {
var logFile = await loadBinary("server.txt", true);
if(logFile.existsSync()){
logFile.deleteSync();
}
var process = await Process.start(
!useLocalNode ? "node" : '"${embeddedNode.path}"',
["index.js"],
workingDirectory: serverLocation.path
);
process.outLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append));
process.errLines.forEach((line) => logFile.writeAsString("$line\n", mode: FileMode.append));
return true;
}catch(exception){
_showEmbeddedError(context, exception.toString());
return false;
}
}
Future<bool> _showServerDownloadInfo(BuildContext context, bool portable) async {
var nodeFuture = compute(downloadServer, portable);
Future<bool> _showServerDownloadInfo(BuildContext context) async {
var nodeFuture = compute(downloadServer, true);
var result = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
@@ -202,9 +274,9 @@ Future<bool> _showServerDownloadInfo(BuildContext context, bool portable) async
);
}
return const InfoLabel(
return InfoLabel(
label: "Downloading lawin server...",
child: SizedBox(
child: const SizedBox(
width: double.infinity,
child: ProgressBar()
)
@@ -229,13 +301,17 @@ Future<bool> _showServerDownloadInfo(BuildContext context, bool portable) async
return result != null && result;
}
void _showEmbeddedError(BuildContext context, String path) {
void _showEmbeddedError(BuildContext context, String error) {
showDialog(
context: context,
builder: (context) => ContentDialog(
content: Text(
"Cannot start server, missing $path",
textAlign: TextAlign.center),
content: SizedBox(
width: double.infinity,
child: Text(
"Cannot start server: $error",
textAlign: TextAlign.center
)
),
actions: [
SizedBox(
width: double.infinity,
@@ -245,4 +321,74 @@ void _showEmbeddedError(BuildContext context, String path) {
))
],
));
}
Future<bool> _showNodeDownloadInfo(BuildContext context) async {
var nodeFuture = compute(downloadNode, true);
var result = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
content: FutureBuilder(
future: nodeFuture,
builder: (context, snapshot) {
if(snapshot.hasError){
return SizedBox(
width: double.infinity,
child: Text("An error occurred while downloading: ${snapshot.error}",
textAlign: TextAlign.center));
}
if(snapshot.hasData){
return const SizedBox(
width: double.infinity,
child: Text("The download was completed successfully!",
textAlign: TextAlign.center)
);
}
return InfoLabel(
label: "Downloading node...",
child: const SizedBox(
width: double.infinity,
child: ProgressBar()
)
);
}
),
actions: [
FutureBuilder(
future: nodeFuture,
builder: (builder, snapshot) => SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(snapshot.hasData && !snapshot.hasError),
child: Text(!snapshot.hasData && !snapshot.hasError ? 'Stop' : 'Close'),
)
)
)
],
)
);
return result != null && result;
}
Future<bool> _showAlreadyBindPortWarning(BuildContext context) async {
return await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
content: const Text(
"Port 3551 is already in use, do you want to kill the associated process?",
textAlign: TextAlign.center),
actions: [
Button(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Close'),
),
FilledButton(
child: const Text('Kill'),
onPressed: () => Navigator.of(context).pop(true)),
],
)) ??
false;
}

View File

@@ -1,4 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
@@ -9,25 +11,16 @@ import 'package:reboot_launcher/src/util/binary.dart';
final serverLocation = Directory("${Platform.environment["UserProfile"]}/.reboot_launcher/lawin");
const String _serverUrl =
"https://github.com/Lawin0129/LawinServer/archive/refs/heads/main.zip";
const String _portableServerUrl =
"https://cdn.discordapp.com/attachments/998020695223193673/1019999251994005504/LawinServer.exe";
Future<bool> downloadServer(bool portable) async {
if(!portable){
var response = await http.get(Uri.parse(_serverUrl));
var tempZip = File("${Platform.environment["Temp"]}/lawin.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, serverLocation.parent.path);
var result = Directory("${serverLocation.parent.path}/LawinServer-main");
await result.rename("${serverLocation.parent.path}/${path.basename(serverLocation.path)}");
await Process.run("${serverLocation.path}/install_packages.bat", [], workingDirectory: serverLocation.path);
await updateEngineConfig();
return true;
}
var response = await http.get(Uri.parse(_portableServerUrl));
var server = await loadBinary("LawinServer.exe", true);
await server.writeAsBytes(response.bodyBytes);
Future<bool> downloadServer(ignored) async {
var response = await http.get(Uri.parse(_serverUrl));
var tempZip = File("${Platform.environment["Temp"]}/lawin.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, serverLocation.parent.path);
var result = Directory("${serverLocation.parent.path}/LawinServer-main");
await result.rename("${serverLocation.parent.path}/${path.basename(serverLocation.path)}");
await Process.run("${serverLocation.path}/install_packages.bat", [], workingDirectory: serverLocation.path);
await updateEngineConfig();
return true;
}
@@ -38,10 +31,9 @@ Future<void> updateEngineConfig() async {
}
Future<bool> isLawinPortFree() async {
return ServerSocket.bind("localhost", 3551)
.then((socket) => socket.close())
.then((_) => true)
.onError((error, _) => false);
var portBat = await loadBinary("port.bat", true);
var process = await Process.run(portBat.path, []);
return !process.outText.contains(" LISTENING "); // Goofy way, best we got
}
List<String> createRebootArgs(String username, bool headless) {
@@ -52,7 +44,6 @@ List<String> createRebootArgs(String username, bool headless) {
"-epicportal",
"-skippatchcheck",
"-fromfl=eac",
"-fltoken=3db3ba5dcbd2e16703f3978d",
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN=$username@projectreboot.dev",
"-AUTH_PASSWORD=Rebooted",
@@ -79,7 +70,8 @@ Future<Uri?> ping(String host, String port, [bool https=false]) async {
..connectionTimeout = const Duration(seconds: 5);
var request = await client.getUrl(uri);
var response = await request.close();
return response.statusCode == 200 ? uri : null;
var body = utf8.decode(await response.single);
return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null;
}catch(_){
return https || declaredScheme != null ? null : await ping(host, port, true);
}

View File

@@ -22,7 +22,6 @@ class AddLocalVersion extends StatelessWidget {
style: const ContentDialogThemeData(
padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
),
constraints: const BoxConstraints(maxWidth: 368, maxHeight: 258),
content: _createLocalVersionDialogBody(),
actions: _createLocalVersionActions(formContext))));
}
@@ -55,6 +54,7 @@ class AddLocalVersion extends StatelessWidget {
Widget _createLocalVersionDialogBody() {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -63,17 +63,11 @@ class AddLocalVersion extends StatelessWidget {
header: "Name",
placeholder: "Type the version's name",
autofocus: true,
validator: (text) {
if (text == null || text.isEmpty) {
return 'Empty version name';
}
validator: _checkVersion
),
if (_gameController.versions.value.any((element) => element.name == text)) {
return 'This version already exists';
}
return null;
},
const SizedBox(
height: 16.0
),
FileSelector(
@@ -83,11 +77,25 @@ class AddLocalVersion extends StatelessWidget {
controller: _gamePathController,
validator: _checkGameFolder,
folder: true
)
),
const SizedBox(height: 8.0),
],
);
}
String? _checkVersion(String? text) {
if (text == null || text.isEmpty) {
return 'Empty version name';
}
if (_gameController.versions.value.any((element) => element.name == text)) {
return 'This version already exists';
}
return null;
}
String? _checkGameFolder(text) {
if (text == null || text.isEmpty) {
return 'Empty game path';

View File

@@ -85,7 +85,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
style: const ContentDialogThemeData(
padding: EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
),
constraints: const BoxConstraints(maxWidth: 368, maxHeight: 321),
content: _createDownloadVersionBody(),
actions: _createDownloadVersionOption(context))));
}
@@ -233,6 +232,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
switch (_status) {
case DownloadStatus.none:
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -242,6 +242,8 @@ class _AddServerVersionState extends State<AddServerVersion> {
VersionNameInput(controller: _nameController),
const SizedBox(height: 16.0),
FileSelector(
label: "Destination",
placeholder: "Type the download destination",
@@ -250,6 +252,8 @@ class _AddServerVersionState extends State<AddServerVersion> {
validator: _checkDownloadDestination,
folder: true
),
const SizedBox(height: 8.0),
],
);
case DownloadStatus.downloading:
@@ -300,11 +304,11 @@ class _AddServerVersionState extends State<AddServerVersion> {
],
);
case DownloadStatus.extracting:
return const Padding(
padding: EdgeInsets.only(bottom: 16),
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: InfoLabel(
label: "Extracting...",
child: SizedBox(width: double.infinity, child: ProgressBar())
child: const SizedBox(width: double.infinity, child: ProgressBar())
),
);
case DownloadStatus.done:

View File

@@ -18,7 +18,7 @@ class _BuildSelectorState extends State<BuildSelector> {
Widget build(BuildContext context) {
return InfoLabel(
label: "Build",
child: Combobox<FortniteBuild>(
child: ComboBox<FortniteBuild>(
placeholder: const Text('Select a fortnite build'),
isExpanded: true,
items: _createItems(),
@@ -29,14 +29,14 @@ class _BuildSelectorState extends State<BuildSelector> {
);
}
List<ComboboxItem<FortniteBuild>> _createItems() {
List<ComboBoxItem<FortniteBuild>> _createItems() {
return _buildController.builds!
.map((element) => _createItem(element))
.toList();
}
ComboboxItem<FortniteBuild> _createItem(FortniteBuild element) {
return ComboboxItem<FortniteBuild>(
ComboBoxItem<FortniteBuild> _createItem(FortniteBuild element) {
return ComboBoxItem<FortniteBuild>(
value: element,
child: Text(
"${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"),

View File

@@ -10,6 +10,7 @@ class FileSelector extends StatefulWidget {
final bool allowNavigator;
final TextEditingController controller;
final String? Function(String?) validator;
final AutovalidateMode? validatorMode;
final String? extension;
final bool folder;
@@ -21,6 +22,7 @@ class FileSelector extends StatefulWidget {
required this.validator,
required this.folder,
this.extension,
this.validatorMode,
this.allowNavigator = true,
Key? key})
: assert(folder || extension != null, "Missing extension for file selector"),
@@ -41,23 +43,20 @@ class _FileSelectorState extends State<FileSelector> {
children: [
Expanded(
child: TextFormBox(
controller: widget.controller,
placeholder: widget.placeholder,
validator: widget.validator,
hidePadding: true
controller: widget.controller,
placeholder: widget.placeholder,
validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
)
),
if (widget.allowNavigator) const SizedBox(width: 8.0),
if (widget.allowNavigator)
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Tooltip(
Tooltip(
message: "Select a ${widget.folder ? 'folder' : 'file'}",
child: Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
),
),
)
],
)
@@ -73,13 +72,13 @@ class _FileSelectorState extends State<FileSelector> {
_selecting = true;
if(widget.folder) {
compute(openFolderPicker, widget.windowTitle)
.then((value) => widget.controller.text = value ?? "")
.then((value) => widget.controller.text = value ?? widget.controller.text)
.then((_) => _selecting = false);
return;
}
compute(openFilePicker, widget.extension!)
.then((value) => widget.controller.text = value ?? "")
.then((value) => widget.controller.text = value ?? widget.controller.text)
.then((_) => _selecting = false);
}
}

View File

@@ -2,73 +2,40 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/widget/smart_switch.dart';
class DeploymentSelector extends StatefulWidget {
const DeploymentSelector({Key? key}) : super(key: key);
@override
State<DeploymentSelector> createState() => _DeploymentSelectorState();
}
class _DeploymentSelectorState extends State<DeploymentSelector> {
final Map<GameType, String> _options = {
GameType.client: "Client",
GameType.server: "Server",
GameType.headlessServer: "Headless Server"
};
final Map<GameType, String> _descriptions = {
GameType.client: "A fortnite client will be launched to play multiplayer games",
GameType.server: "A fortnite client will be launched to host multiplayer games",
GameType.headlessServer: "A fortnite client will be launched in the background to host multiplayer games",
};
class DeploymentSelector extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
bool? _value;
@override
void initState() {
switch(_gameController.type.value){
case GameType.client:
_value = false;
break;
case GameType.server:
_value = true;
break;
case GameType.headlessServer:
_value = null;
break;
}
super.initState();
}
DeploymentSelector({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Tooltip(
message: _descriptions[_gameController.type.value]!,
message: "The type of Fortnite instance to launch",
child: InfoLabel(
label: _options[_gameController.type.value]!,
child: Checkbox(
checked: _value,
onChanged: _onSelected
label: "Type",
child: SizedBox(
width: double.infinity,
child: Obx(() => DropDownButton(
leading: Text(_gameController.type.value.name),
items: GameType.values
.map((type) => _createItem(type))
.toList()))
),
),
);
}
void _onSelected(bool? value){
if(value == null){
_gameController.type(GameType.client);
setState(() => _value = false);
return;
}
if(value){
_gameController.type(GameType.server);
setState(() => _value = true);
return;
}
_gameController.type(GameType.headlessServer);
setState(() => _value = null);
MenuFlyoutItem _createItem(GameType type) {
return MenuFlyoutItem(
text: SizedBox(
width: double.infinity,
child: Tooltip(
message: type.message,
child: Text(type.name)
)
),
onPressed: () => _gameController.type(type)
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/smart_input.dart';
class HostInput extends StatelessWidget {
@@ -10,26 +11,14 @@ class HostInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Obx(() => Tooltip(
message: _serverController.embedded.value
? "The remote lawin host cannot be set when running on embedded"
: "The remote host of the lawin server to use for authentication",
child: _buildInput(context),
));
}
SmartInput _buildInput(BuildContext context) {
return SmartInput(
label: "Host",
placeholder: "Type the host name",
controller: _serverController.host,
enabled: !_serverController.embedded.value,
onTap: () => _serverController.embedded.value
? showSnackbar(
context,
const Snackbar(
content: Text("The host is locked when embedded is on")))
: {},
return Tooltip(
message: "The hostname of the lawin server",
child: Obx(() => SmartInput(
label: "Host",
placeholder: "Type the lawin server's hostname",
controller: _serverController.host,
enabled: _serverController.type.value == ServerType.remote
))
);
}
}

View File

@@ -11,11 +11,14 @@ import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/util/binary.dart';
import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:win32_suspend_process/win32_suspend_process.dart';
import 'package:path/path.dart' as path;
import '../controller/settings_controller.dart';
import '../model/server_type.dart';
import '../util/server_standalone.dart';
class LaunchButton extends StatefulWidget {
@@ -36,7 +39,7 @@ class _LaunchButtonState extends State<LaunchButton> {
@override
void initState() {
loadBinary("log.txt", true)
loadBinary("game.txt", true)
.then((value) => _logFile = value);
super.initState();
}
@@ -137,24 +140,31 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
if(!(await isLawinPortFree())){
_serverController.started(true);
return;
switch(_serverController.type.value){
case ServerType.embedded:
var result = await changeEmbeddedServerState(context, false);
_serverController.started(result);
break;
case ServerType.remote:
_serverController.reverseProxy = await changeReverseProxyState(
context,
_serverController.host.text,
_serverController.port.text,
false,
_serverController.reverseProxy
);
_serverController.started(_serverController.reverseProxy != null);
break;
case ServerType.local:
var result = await checkLocalServer(
context,
_serverController.host.text,
_serverController.port.text,
true
);
_serverController.started(result);
break;
}
if (_serverController.embedded.value) {
var result = await changeEmbeddedServerState(context, false);
_serverController.started(result);
return;
}
_serverController.reverseProxy = await changeReverseProxyState(
context,
_serverController.host.text,
_serverController.port.text,
_serverController.reverseProxy
);
_serverController.started(_serverController.reverseProxy != null);
}
Future<void> _updateServerState(bool value) async {
@@ -212,6 +222,31 @@ class _LaunchButtonState extends State<LaunchButton> {
);
}
Future<void> _showMissingDllError(String name) async {
if(!mounted){
return;
}
showDialog(
context: context,
builder: (context) => ContentDialog(
content: SizedBox(
width: double.infinity,
child: Text("$name dll is not a valid dll, fix it in the settings tab", textAlign: TextAlign.center)
),
actions: [
SizedBox(
width: double.infinity,
child: Button(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
)
)
],
)
);
}
Future<void> _showTokenError() async {
if(!mounted){
return;
@@ -270,9 +305,9 @@ class _LaunchButtonState extends State<LaunchButton> {
var result = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
content: const InfoLabel(
content: InfoLabel(
label: "Launching headless reboot server...",
child: SizedBox(
child: const SizedBox(
width: double.infinity,
child: ProgressBar()
)
@@ -313,7 +348,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
if(line.contains("port 3551 failed: Connection refused")){
if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){
_fail = true;
_closeDialogIfOpen(false);
_showBrokenServerWarning();
@@ -327,7 +362,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
if(line.contains("Network failure when attempting to check platform restrictions")){
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
_fail = true;
_closeDialogIfOpen(false);
_showTokenError();
@@ -384,31 +419,50 @@ class _LaunchButtonState extends State<LaunchButton> {
try {
var dllPath = _getDllPath(injectable);
var success = await injectDll(gameProcess.pid, dllPath);
if (success) {
return;
if(!dllPath.existsSync()) {
await _downloadMissingDll(injectable);
if(!dllPath.existsSync()){
WidgetsBinding.instance.addPostFrameCallback((_) {
_fail = true;
_closeDialogIfOpen(false);
_showMissingDllError(path.basename(dllPath.path));
_onStop();
});
return;
}
}
_onInjectError(injectable.name);
await injectDll(gameProcess.pid, dllPath.path);
} catch (exception) {
_onInjectError(injectable.name);
showSnackbar(
context,
Snackbar(
content: Text("Cannot inject $injectable.dll: $exception", textAlign: TextAlign.center),
extended: true
)
);
_onStop();
}
}
String _getDllPath(Injectable injectable){
File _getDllPath(Injectable injectable){
switch(injectable){
case Injectable.reboot:
return _settingsController.rebootDll.text;
return File(_settingsController.rebootDll.text);
case Injectable.console:
return _settingsController.consoleDll.text;
return File(_settingsController.consoleDll.text);
case Injectable.cranium:
return _settingsController.craniumDll.text;
return File(_settingsController.craniumDll.text);
}
}
void _onInjectError(String binary) {
showSnackbar(context, Snackbar(content: Text("Cannot inject $binary")));
launchUrl(injectLogFile.uri);
Future<void> _downloadMissingDll(Injectable injectable) async {
if(injectable != Injectable.reboot){
await loadBinary("$injectable.dll", true);
return;
}
await downloadRebootDll(0);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/widget/smart_switch.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
class LocalServerSwitch extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
@@ -11,11 +11,32 @@ class LocalServerSwitch extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Tooltip(
message: "Determines whether an embedded or remote lawin server should be used",
child: SmartSwitch(
value: _serverController.embedded,
label: "Embedded",
message: "Determines the type of lawin server to use",
child: InfoLabel(
label: "Type",
child: SizedBox(
width: double.infinity,
child: Obx(() => DropDownButton(
leading: Text(_serverController.type.value.name),
items: ServerType.values
.map((type) => _createItem(type))
.toList()))
),
),
);
}
MenuFlyoutItem _createItem(ServerType type) {
return MenuFlyoutItem(
text: SizedBox(
width: double.infinity,
child: Tooltip(
message: type.message,
child: Text(type.name)
)
),
onPressed: () => _serverController.type(type)
);
}
}

View File

@@ -1,8 +1,10 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/smart_input.dart';
class PortInput extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
@@ -10,25 +12,14 @@ class PortInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Obx(() => Tooltip(
message: _serverController.embedded.value
? "The remote lawin port cannot be set when running on embedded"
: "The remote port of the lawin server to use for authentication",
child: _buildInput(context)));
}
SmartInput _buildInput(BuildContext context) {
return SmartInput(
label: "Port",
placeholder: "Type the host port",
controller: _serverController.port,
enabled: !_serverController.embedded.value,
onTap: () => _serverController.embedded.value
? showSnackbar(
context,
const Snackbar(
content: Text("The port is locked when embedded is on")))
: {},
return Tooltip(
message: "The port of the lawin server",
child: Obx(() => SmartInput(
label: "Port",
placeholder: "Type the lawin server's port",
controller: _serverController.port,
enabled: _serverController.type.value != ServerType.embedded
))
);
}
}

View File

@@ -109,11 +109,11 @@ class _ScanLocalVersionState extends State<ScanLocalVersion> {
}
if(!snapshot.hasData){
return const Padding(
padding: EdgeInsets.only(bottom: 16),
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: InfoLabel(
label: "Searching...",
child: SizedBox(width: double.infinity, child: ProgressBar())
child: const SizedBox(width: double.infinity, child: ProgressBar())
),
);
}

View File

@@ -1,6 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/server.dart';
class ServerButton extends StatelessWidget {
@@ -18,46 +19,71 @@ class ServerButton extends StatelessWidget {
message: _helpMessage,
child: Button(
onPressed: () => _onPressed(context),
child: Text(!_serverController.started.value
? "Start"
: "Stop")),
child: Text(_buttonText())),
)),
),
);
}
String get _helpMessage {
if (_serverController.embedded.value) {
if (_serverController.started.value) {
return "Stop the Lawin server currently running";
}
return "Start a new local Lawin server";
String _buttonText() {
if(_serverController.type.value == ServerType.local){
return "Check";
}
if (_serverController.started.value) {
return "Stop the reverse proxy currently running";
if(_serverController.started.value){
return "Stop";
}
return "Start a reverse proxy targeting the remote Lawin server";
return "Start";
}
String get _helpMessage {
switch(_serverController.type.value){
case ServerType.embedded:
if (_serverController.started.value) {
return "Stop the lawin server currently running";
}
return "Start a new local lawin server";
case ServerType.remote:
if (_serverController.started.value) {
return "Stop the reverse proxy currently running";
}
return "Start a reverse proxy targeting the remote lawin server";
case ServerType.local:
return "Check if a local lawin server is running";
}
}
void _onPressed(BuildContext context) async {
var running = _serverController.started.value;
_serverController.started.value = !running;
if (!_serverController.embedded.value) {
_serverController.reverseProxy = await changeReverseProxyState(
switch(_serverController.type.value){
case ServerType.embedded:
var updatedRunning = await changeEmbeddedServerState(context, running);
_updateStarted(updatedRunning);
break;
case ServerType.remote:
_serverController.reverseProxy = await changeReverseProxyState(
context,
_serverController.host.text,
_serverController.port.text,
false,
_serverController.reverseProxy
);
_updateStarted(_serverController.reverseProxy != null);
break;
case ServerType.local:
var result = await checkLocalServer(
context,
_serverController.host.text,
_serverController.port.text,
_serverController.reverseProxy
);
_updateStarted(_serverController.reverseProxy != null);
return;
false
);
_updateStarted(result);
break;
}
var updatedRunning = await changeEmbeddedServerState(context, running);
_updateStarted(updatedRunning);
}
void _updateStarted(bool updatedRunning) {

View File

@@ -7,7 +7,7 @@ class SmartInput extends StatelessWidget {
final TextInputType type;
final bool enabled;
final VoidCallback? onTap;
final bool populate;
final bool readOnly;
const SmartInput(
{Key? key,
@@ -16,7 +16,7 @@ class SmartInput extends StatelessWidget {
this.label,
this.onTap,
this.enabled = true,
this.populate = false,
this.readOnly = false,
this.type = TextInputType.text})
: super(key: key);
@@ -29,6 +29,7 @@ class SmartInput extends StatelessWidget {
keyboardType: type,
placeholder: placeholder,
onTap: onTap,
readOnly: readOnly,
);
}
}

View File

@@ -35,31 +35,11 @@ class _SmartSwitchState extends State<SmartSwitch> {
Widget _createSwitch() {
return Obx(() => ToggleSwitch(
enabled: widget.enabled,
onDisabledPress: widget.onDisabledPress,
checked: widget.value.value,
onChanged: _onChanged,
style: ToggleSwitchThemeData.standard(ThemeData(
checkedColor: _toolTipColor.withOpacity(_checkedOpacity),
uncheckedColor: _toolTipColor.withOpacity(_uncheckedOpacity),
borderInputColor: _toolTipColor.withOpacity(_uncheckedOpacity),
accentColor: _bodyColor
.withOpacity(widget.value.value
? _checkedOpacity
: _uncheckedOpacity)
.toAccentColor())))
onChanged: _onChanged
)
);
}
Color get _toolTipColor =>
FluentTheme.of(context).brightness.isDark ? Colors.white : Colors.black;
Color get _bodyColor => SystemTheme.accentColor.accent;
double get _checkedOpacity => widget.enabled ? 1 : 0.5;
double get _uncheckedOpacity => widget.enabled ? 0.8 : 0.5;
void _onChanged(bool checked) {
if (!widget.enabled) {
return;

View File

@@ -16,8 +16,7 @@ class UsernameBox extends StatelessWidget {
child: SmartInput(
label: "Username",
placeholder: "Type your ${_gameController.type.value != GameType.client ? 'hosting' : "in-game"} username",
controller: _gameController.username,
populate: true
controller: _gameController.username
),
));
}

View File

@@ -2,8 +2,6 @@ import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'
show showMenu, PopupMenuEntry, PopupMenuItem;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
@@ -88,16 +86,20 @@ class _VersionSelectorState extends State<VersionSelector> {
MenuFlyoutItem _createVersionItem(
BuildContext context, FortniteVersion version) {
return MenuFlyoutItem(
text: Listener(
onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse ||
event.buttons != kSecondaryMouseButton) {
return;
}
text: SizedBox(
width: double.infinity,
child: Listener(
onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse ||
event.buttons != kSecondaryMouseButton) {
return;
}
await _openMenu(context, version, event.position);
},
child: SizedBox(width: double.infinity, child: Text(version.name))),
await _openMenu(context, version, event.position);
},
child: Text(version.name)
)
),
trailing: const Expanded(child: SizedBox()),
onPressed: () => _gameController.selectedVersion = version);
}
@@ -131,14 +133,21 @@ class _VersionSelectorState extends State<VersionSelector> {
Future<void> _openMenu(
BuildContext context, FortniteVersion version, Offset offset) async {
var result = await showMenu(
context: context,
items: <PopupMenuEntry>[
const PopupMenuItem(value: 0, child: Text("Open in explorer")),
const PopupMenuItem(value: 1, child: Text("Delete"))
],
position:
RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, offset.dy),
var result = await showMenu<int?>(
context: context,
offset: offset,
builder: (context) => MenuFlyout(
items: [
MenuFlyoutItem(
text: const Text('Open in explorer'),
onPressed: () => Navigator.of(context).pop(0)
),
MenuFlyoutItem(
text: const Text('Delete'),
onPressed: () => Navigator.of(context).pop(1)
),
],
)
);
switch (result) {