mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 11:12:23 +01:00
Fixed some small things
This commit is contained in:
190
lib/cli.dart
190
lib/cli.dart
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class FortniteVersion {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
32
lib/src/model/server_type.dart
Normal file
32
lib/src/model/server_type.dart
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' : ''}")
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
24
lib/src/util/node.dart
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]'}"),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user