mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 11:12:23 +01:00
update
This commit is contained in:
50
lib/cli.dart
50
lib/cli.dart
@@ -12,6 +12,12 @@ import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
||||
|
||||
late String? username;
|
||||
late GameType type;
|
||||
late bool verbose;
|
||||
late String dll;
|
||||
late FortniteVersion version;
|
||||
late bool autoRestart;
|
||||
|
||||
void main(List<String> args){
|
||||
handleCLI(args);
|
||||
@@ -20,7 +26,7 @@ void main(List<String> args){
|
||||
Future<void> handleCLI(List<String> args) async {
|
||||
stdout.writeln("Reboot Launcher");
|
||||
stdout.writeln("Wrote by Auties00");
|
||||
stdout.writeln("Version 4.4");
|
||||
stdout.writeln("Version 5.3");
|
||||
|
||||
kill();
|
||||
|
||||
@@ -31,16 +37,17 @@ Future<void> handleCLI(List<String> args) async {
|
||||
var parser = ArgParser()
|
||||
..addCommand("list")
|
||||
..addCommand("launch")
|
||||
..addOption("version", defaultsTo: gameJson["version"])
|
||||
..addOption("version")
|
||||
..addOption("username")
|
||||
..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson))
|
||||
..addOption("server-host")
|
||||
..addOption("server-port")
|
||||
..addOption("matchmaking-address")
|
||||
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path)
|
||||
..addOption("type", allowed: getGameTypes(), defaultsTo: getDefaultGameType(gameJson))
|
||||
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
|
||||
..addFlag("log", defaultsTo: false)
|
||||
..addFlag("memory-fix", defaultsTo: false, negatable: true);
|
||||
..addFlag("auto-restart", defaultsTo: false, negatable: true);
|
||||
var result = parser.parse(args);
|
||||
if (result.command?.name == "list") {
|
||||
stdout.writeln("Versions list: ");
|
||||
@@ -49,45 +56,46 @@ Future<void> handleCLI(List<String> args) async {
|
||||
return;
|
||||
}
|
||||
|
||||
var dll = result["dll"];
|
||||
var type = getGameType(result);
|
||||
var username = result["username"];
|
||||
username ??= gameJson["${type == GameType.client ? "game" : "server"}_username"];
|
||||
var verbose = result["log"];
|
||||
dll = result["dll"];
|
||||
type = getGameType(result);
|
||||
username = result["username"] ?? gameJson["${type == GameType.client ? "game" : "server"}_username"];
|
||||
verbose = result["log"];
|
||||
|
||||
var dummyVersion = _createVersion(gameJson["version"], result["version"], result["memory-fix"], versions);
|
||||
version = _createVersion(gameJson["version"], result["version"], versions);
|
||||
await downloadRequiredDLLs();
|
||||
if(result["update"]) {
|
||||
stdout.writeln("Updating reboot dll...");
|
||||
await downloadRebootDll(0);
|
||||
try {
|
||||
await downloadRebootDll(0);
|
||||
}catch(error){
|
||||
stderr.writeln("Cannot update reboot dll: $error");
|
||||
}
|
||||
}
|
||||
|
||||
stdout.writeln("Launching game(type: ${type.name})...");
|
||||
if(dummyVersion.executable == null){
|
||||
throw Exception("Missing game executable at: ${dummyVersion.location.path}");
|
||||
if(version.executable == null){
|
||||
throw Exception("Missing game executable at: ${version.location.path}");
|
||||
}
|
||||
|
||||
if (result["type"] == "headless_server") {
|
||||
await patchHeadless(dummyVersion.executable!);
|
||||
}else if(result["type"] == "client"){
|
||||
await patchMatchmaking(dummyVersion.executable!);
|
||||
}
|
||||
await patchHeadless(version.executable!);
|
||||
await patchMatchmaking(version.executable!);
|
||||
|
||||
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 startServer(host, port, serverType);
|
||||
var started = await startServer(host, port, serverType, result["matchmaking-address"]);
|
||||
if(!started){
|
||||
stderr.writeln("Cannot start server!");
|
||||
return;
|
||||
}
|
||||
|
||||
await startGame(username, type, verbose, dll, dummyVersion);
|
||||
autoRestart = result["auto-restart"];
|
||||
await startGame();
|
||||
}
|
||||
|
||||
FortniteVersion _createVersion(String? versionName, String? versionPath, bool memoryFix, List<FortniteVersion> versions) {
|
||||
FortniteVersion _createVersion(String? versionName, String? versionPath, List<FortniteVersion> versions) {
|
||||
if (versionPath != null) {
|
||||
return FortniteVersion(name: "dummy", location: Directory(versionPath), memoryFix: memoryFix);
|
||||
return FortniteVersion(name: "dummy", location: Directory(versionPath));
|
||||
}
|
||||
|
||||
if(versionName != null){
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
|
||||
show WinDesktopWindow;
|
||||
import 'package:dart_vlc/dart_vlc.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
@@ -16,6 +17,7 @@ import 'package:reboot_launcher/src/page/home_page.dart';
|
||||
import 'package:reboot_launcher/src/util/error.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final GlobalKey appKey = GlobalKey();
|
||||
|
||||
@@ -28,6 +30,7 @@ void main(List<String> args) async {
|
||||
}
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartVLC.initialize();
|
||||
|
||||
await SystemTheme.accentColor.load();
|
||||
await GetStorage.init("game");
|
||||
@@ -39,11 +42,18 @@ void main(List<String> args) async {
|
||||
Get.put(BuildController());
|
||||
Get.put(SettingsController());
|
||||
doWhenWindowReady(() {
|
||||
const size = Size(600, 365);
|
||||
var controller = Get.find<SettingsController>();
|
||||
var size = Size(controller.width, controller.height);
|
||||
var window = appWindow as WinDesktopWindow;
|
||||
window.setWindowCutOnMaximize(appBarSize * 2);
|
||||
appWindow.size = size;
|
||||
appWindow.alignment = Alignment.center;
|
||||
if(controller.offsetX != null && controller.offsetY != null){
|
||||
appWindow.position = Offset(controller.offsetX!, controller.offsetY!);
|
||||
}else {
|
||||
appWindow.alignment = Alignment.center;
|
||||
}
|
||||
|
||||
windowManager.setPreventClose(true);
|
||||
appWindow.title = "Reboot Launcher";
|
||||
appWindow.show();
|
||||
});
|
||||
|
||||
49
lib/src/cli/compatibility.dart
Normal file
49
lib/src/cli/compatibility.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
Future<Map<String, dynamic>> getControllerJson(String name) async {
|
||||
var folder = await _getWindowsPath(FOLDERID_Documents);
|
||||
if(folder == null){
|
||||
throw Exception("Missing documents folder");
|
||||
}
|
||||
|
||||
var file = File("$folder/$name.gs");
|
||||
if(!file.existsSync()){
|
||||
return HashMap();
|
||||
}
|
||||
|
||||
return jsonDecode(file.readAsStringSync());
|
||||
}
|
||||
|
||||
Future<String?> _getWindowsPath(String folderID) {
|
||||
final Pointer<Pointer<Utf16>> pathPtrPtr = calloc<Pointer<Utf16>>();
|
||||
final Pointer<GUID> knownFolderID = calloc<GUID>()..ref.setGUID(folderID);
|
||||
|
||||
try {
|
||||
final int hr = SHGetKnownFolderPath(
|
||||
knownFolderID,
|
||||
KF_FLAG_DEFAULT,
|
||||
NULL,
|
||||
pathPtrPtr,
|
||||
);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
if (hr == E_INVALIDARG || hr == E_FAIL) {
|
||||
throw WindowsException(hr);
|
||||
}
|
||||
return Future<String?>.value();
|
||||
}
|
||||
|
||||
final String path = pathPtrPtr.value.toDartString();
|
||||
return Future<String>.value(path);
|
||||
} finally {
|
||||
calloc.free(pathPtrPtr);
|
||||
calloc.free(knownFolderID);
|
||||
}
|
||||
}
|
||||
52
lib/src/cli/config.dart
Normal file
52
lib/src/cli/config.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
|
||||
import '../model/fortnite_version.dart';
|
||||
import '../model/game_type.dart';
|
||||
import '../model/server_type.dart';
|
||||
|
||||
Iterable<String> getGameTypes() => 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 getGameType(ArgResults result) {
|
||||
var type = GameType.of(result["type"]);
|
||||
if(type == null){
|
||||
throw Exception("Unknown game type: $result. Use --type only with ${getGameTypes().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 getDefaultGameType(Map<String, dynamic> json){
|
||||
var type = GameType.values.elementAt(json["type"] ?? 0);
|
||||
switch(type){
|
||||
case GameType.client:
|
||||
return "client";
|
||||
case GameType.server:
|
||||
return "server";
|
||||
case GameType.headlessServer:
|
||||
return "headless_server";
|
||||
}
|
||||
}
|
||||
|
||||
List<FortniteVersion> getVersions(Map<String, dynamic> gameJson) {
|
||||
Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]");
|
||||
return iterable.map((entry) => FortniteVersion.fromJson(entry))
|
||||
.toList();
|
||||
}
|
||||
137
lib/src/cli/game.dart
Normal file
137
lib/src/cli/game.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/cli.dart';
|
||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
||||
|
||||
import '../model/fortnite_version.dart';
|
||||
import '../model/game_type.dart';
|
||||
import '../util/injector.dart';
|
||||
import '../util/os.dart';
|
||||
import '../util/server.dart';
|
||||
|
||||
final List<String> _errorStrings = [
|
||||
"port 3551 failed: Connection refused",
|
||||
"Unable to login to Fortnite servers",
|
||||
"HTTP 400 response from ",
|
||||
"Network failure when attempting to check platform restrictions",
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
|
||||
Process? _gameProcess;
|
||||
Process? _launcherProcess;
|
||||
Process? _eacProcess;
|
||||
|
||||
Future<void> startGame() async {
|
||||
await _startLauncherProcess(version);
|
||||
await _startEacProcess(version);
|
||||
|
||||
var gamePath = version.executable?.path;
|
||||
if (gamePath == null) {
|
||||
throw Exception("${version.location
|
||||
.path} no longer contains a Fortnite executable, did you delete or move it?");
|
||||
}
|
||||
|
||||
var hosting = type != GameType.client;
|
||||
if (username == null) {
|
||||
username = "Reboot${hosting ? 'Host' : 'Player'}";
|
||||
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
|
||||
}
|
||||
|
||||
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type))
|
||||
..exitCode.then((_) => _onClose())
|
||||
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
|
||||
_injectOrShowError("craniumv2.dll");
|
||||
}
|
||||
|
||||
|
||||
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
||||
if (dummyVersion.launcher == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
|
||||
Win32Process(_launcherProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
||||
if (dummyVersion.eacExecutable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
||||
Win32Process(_eacProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
||||
if(verbose) {
|
||||
stdout.writeln(line);
|
||||
}
|
||||
|
||||
if(line.contains("Platform has ")){
|
||||
_injectOrShowError("craniumv2.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) {
|
||||
_onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if(_errorStrings.any((element) => line.contains(element))){
|
||||
stderr.writeln("The backend doesn't work! Token expired");
|
||||
_onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Region ")){
|
||||
_injectRequiredDLLs(hosting, dll);
|
||||
}
|
||||
}
|
||||
|
||||
void _injectRequiredDLLs(bool host, String rebootDll) {
|
||||
if(host) {
|
||||
_injectOrShowError(rebootDll, false);
|
||||
}else {
|
||||
_injectOrShowError("console.dll");
|
||||
}
|
||||
|
||||
_injectOrShowError("leakv2.dll");
|
||||
}
|
||||
|
||||
void _kill() {
|
||||
_gameProcess?.kill(ProcessSignal.sigabrt);
|
||||
_launcherProcess?.kill(ProcessSignal.sigabrt);
|
||||
_eacProcess?.kill(ProcessSignal.sigabrt);
|
||||
}
|
||||
|
||||
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
|
||||
if (_gameProcess == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
stdout.writeln("Injecting $binary...");
|
||||
var dll = locate ? await loadBinary(binary, true) : File(binary);
|
||||
if(!dll.existsSync()){
|
||||
throw Exception("Cannot inject $dll: missing file");
|
||||
}
|
||||
|
||||
await injectDll(_gameProcess!.pid, dll.path);
|
||||
} catch (exception) {
|
||||
throw Exception("Cannot inject binary: $binary");
|
||||
}
|
||||
}
|
||||
|
||||
void _onClose() {
|
||||
_kill();
|
||||
sleep(const Duration(seconds: 3));
|
||||
stdout.writeln("The game was closed");
|
||||
if(autoRestart){
|
||||
stdout.writeln("Restarting automatically game");
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
59
lib/src/cli/reboot.dart
Normal file
59
lib/src/cli/reboot.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
const String _baseDownload = "https://cdn.discordapp.com/attachments/1009257632315494520/1051137082766131250/Cranium.dll";
|
||||
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll";
|
||||
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1013220721494863872/1033484506633617500/MemoryLeakFixer.dll";
|
||||
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
|
||||
|
||||
Future<void> downloadRequiredDLLs() async {
|
||||
stdout.writeln("Downloading necessary components...");
|
||||
var consoleDll = await loadBinary("console.dll", true);
|
||||
if(!consoleDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_consoleDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download console.dll");
|
||||
}
|
||||
|
||||
await consoleDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var craniumDll = await loadBinary("craniumv2.dll", true);
|
||||
if(!craniumDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_baseDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download craniumv2.dll");
|
||||
}
|
||||
|
||||
await craniumDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var memoryFixDll = await loadBinary("leakv2.dll", true);
|
||||
if(!memoryFixDll.existsSync()){
|
||||
var response = await http.get(Uri.parse(_memoryFixDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download leakv2.dll");
|
||||
}
|
||||
|
||||
await memoryFixDll.writeAsBytes(response.bodyBytes);
|
||||
}
|
||||
|
||||
var config = loadEmbedded("config/");
|
||||
var profiles = loadEmbedded("profiles/");
|
||||
var responses = loadEmbedded("responses/");
|
||||
if(!config.existsSync() || !profiles.existsSync() || !responses.existsSync()){
|
||||
var response = await http.get(Uri.parse(_embeddedConfigDownload));
|
||||
if(response.statusCode != 200){
|
||||
throw Exception("Cannot download embedded server config");
|
||||
}
|
||||
|
||||
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
|
||||
await extractFileToDisk(tempZip.path, "$safeBinariesDirectory\\backend\\cli");
|
||||
}
|
||||
}
|
||||
88
lib/src/cli/server.dart
Normal file
88
lib/src/cli/server.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/embedded/server.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
|
||||
import '../model/server_type.dart';
|
||||
import '../util/server.dart';
|
||||
import 'game.dart';
|
||||
|
||||
Future<bool> startServer(String? host, String? port, ServerType type, String? matchmakingIp) async {
|
||||
stdout.writeln("Starting backend server...");
|
||||
switch(type){
|
||||
case ServerType.local:
|
||||
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
|
||||
if(result == null){
|
||||
throw Exception("Local backend server is not running");
|
||||
}
|
||||
|
||||
stdout.writeln("Detected local backend server");
|
||||
return true;
|
||||
case ServerType.embedded:
|
||||
stdout.writeln("Starting an embedded server...");
|
||||
await startEmbeddedServer(
|
||||
() => matchmakingIp ?? "127.0.0.1"
|
||||
);
|
||||
await startEmbeddedMatchmaker();
|
||||
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
|
||||
if(result == null){
|
||||
throw Exception("Cannot start embedded server");
|
||||
}
|
||||
|
||||
return true;
|
||||
case ServerType.remote:
|
||||
if(host == null){
|
||||
throw Exception("Missing host for remote server");
|
||||
}
|
||||
|
||||
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<HttpServer?> _changeReverseProxyState(String host, String port) async {
|
||||
host = host.trim();
|
||||
if(host.isEmpty){
|
||||
throw Exception("Missing host name");
|
||||
}
|
||||
|
||||
port = port.trim();
|
||||
if(port.isEmpty){
|
||||
throw Exception("Missing port");
|
||||
}
|
||||
|
||||
if(int.tryParse(port) == null){
|
||||
throw Exception("Invalid port, use only numbers");
|
||||
}
|
||||
|
||||
try{
|
||||
var uri = await ping(host, port);
|
||||
if(uri == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
}catch(error){
|
||||
throw Exception("Cannot start reverse proxy");
|
||||
}
|
||||
}
|
||||
|
||||
void kill() 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(_){
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,23 @@ import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
class BuildController extends GetxController {
|
||||
List<FortniteBuild>? builds;
|
||||
FortniteBuild? _selectedBuild;
|
||||
final List<Function()> _listeners;
|
||||
late RxBool cancelledDownload;
|
||||
|
||||
BuildController() {
|
||||
BuildController() : _listeners = [] {
|
||||
cancelledDownload = RxBool(false);
|
||||
}
|
||||
|
||||
FortniteBuild get selectedBuild => _selectedBuild ?? builds!.elementAt(0);
|
||||
|
||||
set selectedBuild(FortniteBuild build) => _selectedBuild = build;
|
||||
set selectedBuild(FortniteBuild build) {
|
||||
_selectedBuild = build;
|
||||
for (var listener in _listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
void addOnBuildChangedListener(Function() listener) => _listeners.add(listener);
|
||||
|
||||
void removeOnBuildChangedListener() => _listeners.clear();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
@@ -90,7 +91,7 @@ class GameController extends GetxController {
|
||||
_storage.write("version", version?.name);
|
||||
}
|
||||
|
||||
void rename(FortniteVersion version, String result) {
|
||||
versions.update((val) => version.name = result);
|
||||
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
|
||||
versions.update((val) => function(version));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ import 'dart:io';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
import '../dialog/snackbar.dart';
|
||||
import '../model/server_type.dart';
|
||||
|
||||
class ServerController extends GetxController {
|
||||
@@ -19,9 +17,9 @@ class ServerController extends GetxController {
|
||||
late final Rx<ServerType> type;
|
||||
late final RxBool warning;
|
||||
late RxBool started;
|
||||
late int embeddedServerCounter;
|
||||
Process? embeddedServer;
|
||||
HttpServer? reverseProxy;
|
||||
Jaguar? embeddedServer;
|
||||
Jaguar? embeddedMatchmaker;
|
||||
HttpServer? remoteServer;
|
||||
|
||||
ServerController() {
|
||||
_storage = GetStorage("server");
|
||||
@@ -37,8 +35,8 @@ class ServerController extends GetxController {
|
||||
}
|
||||
|
||||
if(value == ServerType.remote){
|
||||
reverseProxy?.close(force: true);
|
||||
reverseProxy = null;
|
||||
remoteServer?.close(force: true);
|
||||
remoteServer = null;
|
||||
started.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -56,8 +54,6 @@ class ServerController extends GetxController {
|
||||
warning.listen((value) => _storage.write("lawin_value", value));
|
||||
|
||||
started = RxBool(false);
|
||||
|
||||
embeddedServerCounter = 0;
|
||||
}
|
||||
|
||||
String _readHost() {
|
||||
@@ -70,83 +66,16 @@ class ServerController extends GetxController {
|
||||
return _storage.read("${type.value.id}_port") ?? _serverPort;
|
||||
}
|
||||
|
||||
Future<ServerResult> start(bool needsFreePort) async {
|
||||
var lastCounter = ++embeddedServerCounter;
|
||||
var result = await checkServerPreconditions(host.text, port.text, type.value, needsFreePort);
|
||||
if(result.type != ServerResultType.canStart){
|
||||
return result;
|
||||
}
|
||||
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
await _startEmbeddedServer();
|
||||
embeddedServer?.exitCode.then((value) async {
|
||||
if (!started() || lastCounter != embeddedServerCounter) {
|
||||
return;
|
||||
}
|
||||
|
||||
started.value = false;
|
||||
await freeLawinPort();
|
||||
showUnexpectedError();
|
||||
});
|
||||
break;
|
||||
case ServerType.remote:
|
||||
var uriResult = await result.uri!;
|
||||
if(uriResult == null){
|
||||
return ServerResult(
|
||||
type: ServerResultType.cannotPingServer
|
||||
);
|
||||
}
|
||||
|
||||
reverseProxy = await startRemoteServer(uriResult);
|
||||
break;
|
||||
case ServerType.local:
|
||||
break;
|
||||
}
|
||||
}catch(error, stackTrace){
|
||||
return ServerResult(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
type: ServerResultType.unknownError
|
||||
);
|
||||
}
|
||||
|
||||
var myself = await pingSelf(port.text);
|
||||
if(myself == null){
|
||||
return ServerResult(
|
||||
type: ServerResultType.cannotPingServer,
|
||||
pid: embeddedServer?.pid
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
type: ServerResultType.started
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startEmbeddedServer() async {
|
||||
var result = await startEmbeddedServer();
|
||||
if(result != null){
|
||||
embeddedServer = result;
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage("The server is corrupted, trying to fix it");
|
||||
await serverLocation.parent.delete(recursive: true);
|
||||
await downloadServerInteractive(true);
|
||||
await _startEmbeddedServer();
|
||||
}
|
||||
|
||||
Future<bool> stop() async {
|
||||
started.value = false;
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
await freeLawinPort();
|
||||
await embeddedServer?.close();
|
||||
await embeddedMatchmaker?.close();
|
||||
break;
|
||||
case ServerType.remote:
|
||||
await reverseProxy?.close(force: true);
|
||||
await remoteServer?.close(force: true);
|
||||
break;
|
||||
case ServerType.local:
|
||||
break;
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
|
||||
import 'package:dart_vlc/dart_vlc.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:ini/ini.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
class SettingsController extends GetxController {
|
||||
late final GetStorage _storage;
|
||||
late final String originalDll;
|
||||
late final TextEditingController rebootDll;
|
||||
late final TextEditingController consoleDll;
|
||||
late final TextEditingController craniumDll;
|
||||
late final TextEditingController authDll;
|
||||
late final TextEditingController matchmakingIp;
|
||||
late final Rx<PaneDisplayMode> displayType;
|
||||
late double width;
|
||||
late double height;
|
||||
late double? offsetX;
|
||||
late double? offsetY;
|
||||
Player? player;
|
||||
|
||||
SettingsController() {
|
||||
_storage = GetStorage("settings");
|
||||
|
||||
rebootDll = _createController("reboot", "reboot.dll");
|
||||
consoleDll = _createController("console", "console.dll");
|
||||
craniumDll = _createController("cranium", "cranium.dll");
|
||||
authDll = _createController("cranium2", "craniumv2.dll");
|
||||
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
|
||||
matchmakingIp.addListener(() async {
|
||||
var text = matchmakingIp.text;
|
||||
_storage.write("ip", text);
|
||||
if(await serverConfig.exists()){
|
||||
var config = Config.fromString(await serverConfig.readAsString());
|
||||
if(text.contains(":")){
|
||||
config.set("GameServer", "ip", text.substring(0, text.indexOf(":")));
|
||||
config.set("GameServer", "port", text.substring(text.indexOf(":") + 1));
|
||||
}else {
|
||||
config.set("GameServer", "ip", text);
|
||||
config.set("GameServer", "port", "7777");
|
||||
}
|
||||
|
||||
serverConfig.writeAsString(config.toString());
|
||||
}
|
||||
});
|
||||
|
||||
width = _storage.read("width") ?? window.physicalSize.width;
|
||||
height = _storage.read("height") ?? window.physicalSize.height;
|
||||
offsetX = _storage.read("offset_x");
|
||||
offsetY = _storage.read("offset_y");
|
||||
displayType = Rx(PaneDisplayMode.top);
|
||||
}
|
||||
|
||||
TextEditingController _createController(String key, String name) {
|
||||
@@ -47,4 +46,14 @@ class SettingsController extends GetxController {
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
void saveWindowSize() {
|
||||
_storage.write("width", window.physicalSize.width);
|
||||
_storage.write("height", window.physicalSize.height);
|
||||
}
|
||||
|
||||
void saveWindowOffset(Offset position) {
|
||||
_storage.write("offset_x", position.dx);
|
||||
_storage.write("offset_y", position.dy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ class AddLocalVersion extends StatelessWidget {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _gamePathController = TextEditingController();
|
||||
final CheckboxController _injectMemoryFixController = CheckboxController();
|
||||
|
||||
AddLocalVersion({Key? key})
|
||||
: super(key: key);
|
||||
@@ -49,15 +48,6 @@ class AddLocalVersion extends StatelessWidget {
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
SmartCheckBox(
|
||||
controller: _injectMemoryFixController,
|
||||
content: const Text("Inject memory leak fix")
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
@@ -70,10 +60,10 @@ class AddLocalVersion extends StatelessWidget {
|
||||
text: "Save",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_gamePathController.text),
|
||||
memoryFix: _injectMemoryFixController.value
|
||||
location: Directory(_gamePathController.text)
|
||||
));
|
||||
},
|
||||
)
|
||||
|
||||
@@ -12,11 +12,11 @@ import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/util/build.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/version_name_input.dart';
|
||||
import 'package:universal_disk_space/universal_disk_space.dart';
|
||||
|
||||
import '../util/checks.dart';
|
||||
import '../widget/home/build_selector.dart';
|
||||
import '../widget/shared/file_selector.dart';
|
||||
import '../widget/shared/smart_check_box.dart';
|
||||
import 'dialog.dart';
|
||||
|
||||
class AddServerVersion extends StatefulWidget {
|
||||
@@ -31,21 +31,29 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _pathController = TextEditingController();
|
||||
final CheckboxController _injectMemoryFixController = CheckboxController();
|
||||
late Future _future;
|
||||
DownloadStatus _status = DownloadStatus.none;
|
||||
|
||||
late DiskSpace _diskSpace;
|
||||
late Future _fetchFuture;
|
||||
late Future _diskFuture;
|
||||
|
||||
DownloadStatus _status = DownloadStatus.form;
|
||||
String _timeLeft = "00:00:00";
|
||||
double _downloadProgress = 0;
|
||||
String? _error;
|
||||
Process? _manifestDownloadProcess;
|
||||
CancelableOperation? _driveDownloadOperation;
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_future = _buildController.builds != null
|
||||
_fetchFuture = _buildController.builds != null
|
||||
? Future.value(true)
|
||||
: compute(fetchBuilds, null)
|
||||
.then((value) => _buildController.builds = value);
|
||||
_diskSpace = DiskSpace();
|
||||
_diskFuture = _diskSpace.scan()
|
||||
.then((_) => _updateFormDefaults());
|
||||
_buildController.addOnBuildChangedListener(() => _updateFormDefaults());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -53,6 +61,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
void dispose() {
|
||||
_pathController.dispose();
|
||||
_nameController.dispose();
|
||||
_buildController.removeOnBuildChangedListener();
|
||||
_onDisposed();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -80,74 +89,58 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormDialog(
|
||||
content: _createDownloadVersionBody(),
|
||||
buttons: _createDownloadVersionOption(context)
|
||||
);
|
||||
switch(_status){
|
||||
case DownloadStatus.form:
|
||||
return _createFormDialog();
|
||||
case DownloadStatus.downloading:
|
||||
return GenericDialog(
|
||||
header: _createDownloadBody(),
|
||||
buttons: _createCloseButton()
|
||||
);
|
||||
case DownloadStatus.extracting:
|
||||
return GenericDialog(
|
||||
header: _createExtractingBody(),
|
||||
buttons: _createCloseButton()
|
||||
);
|
||||
case DownloadStatus.error:
|
||||
return ErrorDialog(
|
||||
exception: _error ?? Exception("unknown error"),
|
||||
stackTrace: _stackTrace,
|
||||
errorMessageBuilder: (exception) => "Cannot download version: $exception"
|
||||
);
|
||||
case DownloadStatus.done:
|
||||
return const InfoDialog(
|
||||
text: "The download was completed successfully!",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<DialogButton> _createDownloadVersionOption(BuildContext context) {
|
||||
switch (_status) {
|
||||
case DownloadStatus.none:
|
||||
return [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: "Download",
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
|
||||
case DownloadStatus.error:
|
||||
return [
|
||||
DialogButton(
|
||||
type: ButtonType.only,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
)
|
||||
];
|
||||
default:
|
||||
return [
|
||||
DialogButton(
|
||||
text: _status == DownloadStatus.downloading ? "Stop" : "Close",
|
||||
type: ButtonType.only)
|
||||
];
|
||||
}
|
||||
List<DialogButton> _createFormButtons() {
|
||||
return [
|
||||
DialogButton(type: ButtonType.secondary),
|
||||
DialogButton(
|
||||
text: "Download",
|
||||
type: ButtonType.primary,
|
||||
onTap: () => _startDownload(context),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
void _startDownload(BuildContext context) async {
|
||||
try {
|
||||
setState(() => _status = DownloadStatus.downloading);
|
||||
if (_buildController.selectedBuild.hasManifest) {
|
||||
_manifestDownloadProcess = await downloadManifestBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
_pathController.text,
|
||||
_onDownloadProgress
|
||||
);
|
||||
_manifestDownloadProcess!.exitCode
|
||||
.then((value) => _onDownloadComplete());
|
||||
} else {
|
||||
_driveDownloadOperation = CancelableOperation.fromFuture(
|
||||
downloadArchiveBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
_pathController.text,
|
||||
(progress) => _onDownloadProgress(progress, _timeLeft),
|
||||
_onUnrar)
|
||||
).then((_) => _onDownloadComplete(),
|
||||
onError: (error, _) => _handleError(error));
|
||||
}
|
||||
} catch (exception) {
|
||||
_handleError(exception);
|
||||
_manifestDownloadProcess = await downloadManifestBuild(
|
||||
_buildController.selectedBuild.link,
|
||||
_pathController.text,
|
||||
_onDownloadProgress
|
||||
);
|
||||
_manifestDownloadProcess!.exitCode
|
||||
.then((value) => _onDownloadComplete());
|
||||
} catch (exception, stackTrace) {
|
||||
_onDownloadError(exception, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr? _handleError(Object exception) {
|
||||
var message = exception.toString();
|
||||
_onDownloadError(message.contains(":")
|
||||
? " ${message.substring(message.indexOf(":") + 1)}"
|
||||
: message);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onUnrar() {
|
||||
setState(() => _status = DownloadStatus.extracting);
|
||||
}
|
||||
@@ -161,20 +154,20 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
_status = DownloadStatus.done;
|
||||
_gameController.addVersion(FortniteVersion(
|
||||
name: _nameController.text,
|
||||
location: Directory(_pathController.text),
|
||||
memoryFix: _injectMemoryFixController.value
|
||||
location: Directory(_pathController.text)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
void _onDownloadError(String message) {
|
||||
void _onDownloadError(Object? error, StackTrace? stackTrace) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_status = DownloadStatus.error;
|
||||
_error = message;
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,70 +183,6 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _createDownloadVersionBody() {
|
||||
return FutureBuilder(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => setState(() => _status = DownloadStatus.error));
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text("Cannot fetch builds: ${snapshot.error}",
|
||||
textAlign: TextAlign.center),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return InfoLabel(
|
||||
label: "Fetching builds...",
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildBody();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
switch (_status) {
|
||||
case DownloadStatus.none:
|
||||
return _createNoneBody();
|
||||
case DownloadStatus.downloading:
|
||||
return _createDownloadBody();
|
||||
case DownloadStatus.extracting:
|
||||
return _createExtractingBody();
|
||||
case DownloadStatus.done:
|
||||
return _createDoneBody();
|
||||
case DownloadStatus.error:
|
||||
return _createErrorBody();
|
||||
}
|
||||
}
|
||||
|
||||
Padding _createErrorBody() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text("An error occurred while downloading:$_error",
|
||||
textAlign: TextAlign.center)),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _createDoneBody() {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text("The download was completed successfully!",
|
||||
textAlign: TextAlign.center)),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _createExtractingBody() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
@@ -263,53 +192,77 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
);
|
||||
}
|
||||
|
||||
Column _createDownloadBody() {
|
||||
Widget _createDownloadBody() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Downloading...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
if(_manifestDownloadProcess != null)
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Downloading...",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${_downloadProgress.round()}%",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
Text(
|
||||
"Time left: $_timeLeft",
|
||||
style: FluentTheme.maybeOf(context)?.typography.body,
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ProgressBar(value: _downloadProgress.toDouble())),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createFormDialog() {
|
||||
return FutureBuilder(
|
||||
future: Future.wait([_fetchFuture, _diskFuture]),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) =>
|
||||
_onDownloadError(snapshot.error, snapshot.stackTrace));
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return ProgressDialog(
|
||||
text: "Fetching builds and disks...",
|
||||
onStop: () => Navigator.of(context).pop()
|
||||
);
|
||||
}
|
||||
|
||||
return FormDialog(
|
||||
content: _createFormBody(),
|
||||
buttons: _createFormButtons()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Column _createNoneBody() {
|
||||
Widget _createFormBody() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -327,15 +280,32 @@ class _AddServerVersionState extends State<AddServerVersion> {
|
||||
validator: checkDownloadDestination,
|
||||
folder: true
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
SmartCheckBox(
|
||||
controller: _injectMemoryFixController,
|
||||
content: const Text("Inject memory leak fix")
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<DialogButton> _createCloseButton() {
|
||||
return [
|
||||
DialogButton(
|
||||
text: "Stop",
|
||||
type: ButtonType.only
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _updateFormDefaults() async {
|
||||
if(_diskSpace.disks.isEmpty){
|
||||
return;
|
||||
}
|
||||
|
||||
await _fetchFuture;
|
||||
var bestDisk = _diskSpace.disks
|
||||
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
|
||||
_pathController.text = "${bestDisk.devicePath}\\FortniteBuilds\\Fortnite "
|
||||
"${_buildController.selectedBuild.version.toString()}";
|
||||
_nameController.text = _buildController.selectedBuild.version.toString();
|
||||
}
|
||||
}
|
||||
|
||||
enum DownloadStatus { none, downloading, extracting, error, done }
|
||||
enum DownloadStatus { form, downloading, extracting, error, done }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -21,12 +22,20 @@ class GenericDialog extends AbstractDialog {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContentDialog(
|
||||
style: ContentDialogThemeData(
|
||||
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
|
||||
return Stack(
|
||||
children: [
|
||||
MoveWindow(
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
content: header,
|
||||
actions: buttons
|
||||
|
||||
ContentDialog(
|
||||
style: ContentDialogThemeData(
|
||||
padding: padding ?? const EdgeInsets.only(left: 20, right: 20, top: 15.0, bottom: 5.0)
|
||||
),
|
||||
content: header,
|
||||
actions: buttons
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,19 +140,21 @@ class ProgressDialog extends AbstractDialog {
|
||||
class FutureBuilderDialog extends AbstractDialog {
|
||||
final Future future;
|
||||
final String loadingMessage;
|
||||
final Widget loadedBody;
|
||||
final Widget successfulBody;
|
||||
final Widget unsuccessfulBody;
|
||||
final Function(Object) errorMessageBuilder;
|
||||
final Function()? onError;
|
||||
final bool closeAutomatically;
|
||||
|
||||
const FutureBuilderDialog(
|
||||
{super.key,
|
||||
required this.future,
|
||||
required this.loadingMessage,
|
||||
required this.loadedBody,
|
||||
required this.errorMessageBuilder,
|
||||
this.onError,
|
||||
this.closeAutomatically = false});
|
||||
required this.future,
|
||||
required this.loadingMessage,
|
||||
required this.successfulBody,
|
||||
required this.unsuccessfulBody,
|
||||
required this.errorMessageBuilder,
|
||||
this.onError,
|
||||
this.closeAutomatically = false});
|
||||
|
||||
static Container ofMessage(String message) {
|
||||
return Container(
|
||||
@@ -170,31 +181,43 @@ class FutureBuilderDialog extends AbstractDialog {
|
||||
Widget _createBody(BuildContext context, AsyncSnapshot snapshot){
|
||||
if (snapshot.hasError) {
|
||||
onError?.call();
|
||||
return ofMessage(snapshot.error.toString());
|
||||
return ofMessage(errorMessageBuilder(snapshot.error!));
|
||||
}
|
||||
|
||||
if(snapshot.connectionState == ConnectionState.done && (snapshot.data == null || (snapshot.data is bool && !snapshot.data))){
|
||||
return unsuccessfulBody;
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return InfoLabel(
|
||||
label: loadingMessage,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()),
|
||||
);
|
||||
return _createLoadingBody();
|
||||
}
|
||||
|
||||
if(closeAutomatically){
|
||||
Navigator.of(context).pop(true);
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => Navigator.of(context).pop(true));
|
||||
return _createLoadingBody();
|
||||
}
|
||||
|
||||
return loadedBody;
|
||||
return successfulBody;
|
||||
}
|
||||
|
||||
InfoLabel _createLoadingBody() {
|
||||
return InfoLabel(
|
||||
label: loadingMessage,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
width: double.infinity,
|
||||
child: const ProgressBar()),
|
||||
);
|
||||
}
|
||||
|
||||
DialogButton _createButton(BuildContext context, AsyncSnapshot snapshot){
|
||||
return DialogButton(
|
||||
text: snapshot.hasData || snapshot.hasError ? "Close" : "Stop",
|
||||
type: ButtonType.only,
|
||||
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
|
||||
text: snapshot.hasData
|
||||
|| snapshot.hasError
|
||||
|| (snapshot.connectionState == ConnectionState.done && snapshot.data == null) ? "Close" : "Stop",
|
||||
type: ButtonType.only,
|
||||
onTap: () => Navigator.of(context).pop(!snapshot.hasError && snapshot.hasData)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ Future<void> showBrokenError() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "The lawin server is not working correctly"
|
||||
text: "The backend server is not working correctly"
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -20,20 +20,26 @@ Future<void> showMissingDllError(String name) async {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showTokenError() async {
|
||||
Future<void> showTokenErrorFixable() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "A token error occurred, restart the game and the lawin server, then try again"
|
||||
text: "A token error occurred. "
|
||||
"The backend server has been automatically restarted to fix the issue. "
|
||||
"Relaunch your game to check if the issue has been automatically fixed. "
|
||||
"Otherwise, open an issue on Discord."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showUnsupportedHeadless() async {
|
||||
Future<void> showTokenErrorUnfixable() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => const InfoDialog(
|
||||
text: "This version of Fortnite doesn't support headless hosting"
|
||||
text: "A token error occurred. "
|
||||
"This issue cannot be resolved automatically as the server isn't embedded."
|
||||
"Please restart the server manually, then relaunch your game to check if the issue has been fixed. "
|
||||
"Otherwise, open an issue on Discord."
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/embedded/server.dart';
|
||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||
import 'package:reboot_launcher/src/util/future.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../util/server.dart';
|
||||
@@ -14,26 +17,31 @@ import '../util/server.dart';
|
||||
extension ServerControllerDialog on ServerController {
|
||||
static Semaphore semaphore = Semaphore();
|
||||
|
||||
Future<bool> changeStateInteractive(bool onlyIfNeeded, [bool isRetry = false]) async {
|
||||
Future<bool> start({required bool required, required bool askPortKill, bool isRetry = false}) async {
|
||||
try{
|
||||
semaphore.acquire();
|
||||
if (type() == ServerType.local) {
|
||||
return _checkLocalServerInteractive(onlyIfNeeded);
|
||||
return _pingSelfInteractive(required);
|
||||
}
|
||||
|
||||
var oldStarted = started();
|
||||
if(oldStarted && onlyIfNeeded){
|
||||
if(oldStarted && required){
|
||||
return true;
|
||||
}
|
||||
|
||||
started.value = !started.value;
|
||||
return await _doStateChange(oldStarted, onlyIfNeeded, isRetry);
|
||||
var result = await _startInternal(oldStarted, required, askPortKill, isRetry);
|
||||
if(!result){
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _pingSelfInteractive(true);
|
||||
}finally{
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _doStateChange(bool oldStarted, bool onlyIfNeeded, bool isRetry) async {
|
||||
Future<bool> _startInternal(bool oldStarted, bool required, bool askPortKill, bool isRetry) async {
|
||||
if (oldStarted) {
|
||||
var result = await stop();
|
||||
if (!result) {
|
||||
@@ -45,13 +53,14 @@ extension ServerControllerDialog on ServerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await start(!onlyIfNeeded);
|
||||
if(result.type == ServerResultType.ignoreStart) {
|
||||
var conditions = await checkServerPreconditions(host.text, port.text, type.value, !required);
|
||||
var result = conditions.type == ServerResultType.canStart ? await _startServer(required) : conditions;
|
||||
if(result.type == ServerResultType.alreadyStarted) {
|
||||
started.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var handled = await _handleResultType(oldStarted, onlyIfNeeded, isRetry, result);
|
||||
var handled = await _handleResultType(oldStarted, required, isRetry, askPortKill, result);
|
||||
if (!handled) {
|
||||
started.value = false;
|
||||
return false;
|
||||
@@ -60,7 +69,42 @@ extension ServerControllerDialog on ServerController {
|
||||
return handled;
|
||||
}
|
||||
|
||||
Future<bool> _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, ServerResult result) async {
|
||||
Future<ServerResult> _startServer(bool closeAutomatically) async {
|
||||
try{
|
||||
switch(type()){
|
||||
case ServerType.embedded:
|
||||
embeddedServer = await startEmbeddedServer(
|
||||
() => Get.find<SettingsController>().matchmakingIp.text,
|
||||
);
|
||||
embeddedMatchmaker = await startEmbeddedMatchmaker();
|
||||
break;
|
||||
case ServerType.remote:
|
||||
var uriResult = await _pingRemoteInteractive(closeAutomatically);
|
||||
if(uriResult == null){
|
||||
return ServerResult(
|
||||
type: ServerResultType.cannotPingServer
|
||||
);
|
||||
}
|
||||
|
||||
remoteServer = await startRemoteServer(uriResult);
|
||||
break;
|
||||
case ServerType.local:
|
||||
break;
|
||||
}
|
||||
}catch(error, stackTrace){
|
||||
return ServerResult(
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
type: ServerResultType.unknownError
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
type: ServerResultType.canStart
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, bool askPortKill, ServerResult result) async {
|
||||
switch (result.type) {
|
||||
case ServerResultType.missingHostError:
|
||||
_showMissingHostError();
|
||||
@@ -72,11 +116,6 @@ extension ServerControllerDialog on ServerController {
|
||||
_showIllegalPortError();
|
||||
return false;
|
||||
case ServerResultType.cannotPingServer:
|
||||
if(!started() || result.pid != embeddedServer?.pid){
|
||||
return false;
|
||||
}
|
||||
|
||||
_showPingErrorDialog();
|
||||
return false;
|
||||
case ServerResultType.portTakenError:
|
||||
if (isRetry) {
|
||||
@@ -84,24 +123,15 @@ extension ServerControllerDialog on ServerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await _showPortTakenDialog();
|
||||
if (!result) {
|
||||
return false;
|
||||
if(askPortKill) {
|
||||
var result = await _showPortTakenDialog();
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await freeLawinPort();
|
||||
return _doStateChange(oldStarted, onlyIfNeeded, true);
|
||||
case ServerResultType.serverDownloadRequiredError:
|
||||
if (isRetry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = await downloadServerInteractive(false);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _doStateChange(oldStarted, onlyIfNeeded, true);
|
||||
return _startInternal(oldStarted, onlyIfNeeded, askPortKill, true);
|
||||
case ServerResultType.unknownError:
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
@@ -114,39 +144,67 @@ extension ServerControllerDialog on ServerController {
|
||||
)
|
||||
);
|
||||
return false;
|
||||
case ServerResultType.ignoreStart:
|
||||
case ServerResultType.started:
|
||||
return true;
|
||||
case ServerResultType.alreadyStarted:
|
||||
case ServerResultType.canStart:
|
||||
return true;
|
||||
case ServerResultType.stopped:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _checkLocalServerInteractive(bool ignorePrompts) async {
|
||||
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
|
||||
try {
|
||||
var future = pingSelf(port.text);
|
||||
if(!ignorePrompts) {
|
||||
await showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: future,
|
||||
loadingMessage: "Pinging server...",
|
||||
loadedBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} works correctly"),
|
||||
errorMessageBuilder: (
|
||||
exception) => "An error occurred while pining the server: $exception"
|
||||
)
|
||||
);
|
||||
}
|
||||
return await future != null;
|
||||
return await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: Future.wait([
|
||||
pingSelf(port.text),
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
]),
|
||||
loadingMessage: "Pinging ${type().id} server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The ${type().id} server works correctly"),
|
||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
|
||||
errorMessageBuilder: (
|
||||
exception) => "An error occurred while pining the ${type().id} server: $exception",
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
) ?? false;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri?> _pingRemoteInteractive(bool closeAutomatically) async {
|
||||
try {
|
||||
var mainFuture = ping(host.text, port.text);
|
||||
var result = await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: Future.wait([
|
||||
mainFuture,
|
||||
Future.delayed(const Duration(seconds: 1))
|
||||
]),
|
||||
loadingMessage: "Pinging remote server...",
|
||||
successfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} works correctly"),
|
||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
||||
"The server at ${host.text}:${port
|
||||
.text} doesn't work. Check the hostname and/or the port and try again."),
|
||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception",
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
) ?? false;
|
||||
return result ? await mainFuture : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showPortTakenError() async {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
@@ -178,20 +236,6 @@ extension ServerControllerDialog on ServerController {
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
void _showPingErrorDialog() {
|
||||
if(!started.value){
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
const InfoDialog(
|
||||
text: "The lawin server is not working correctly. Check the configuration in the associated tab and try again."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _showCannotStopError() {
|
||||
if(!started.value){
|
||||
return;
|
||||
@@ -201,47 +245,45 @@ extension ServerControllerDialog on ServerController {
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
const InfoDialog(
|
||||
text: "Cannot stop lawin server"
|
||||
text: "Cannot stop backend server"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void showUnexpectedError() {
|
||||
void showUnexpectedServerError() {
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
const InfoDialog(
|
||||
text: "The lawin server died unexpectedly"
|
||||
builder: (context) => InfoDialog(
|
||||
text: "The backend server died unexpectedly",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
text: "Close",
|
||||
type: ButtonType.secondary,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: "Open log",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
launchUrl(serverLogFile.uri);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _showIllegalPortError() {
|
||||
showMessage("Illegal port for lawin server, use only numbers");
|
||||
showMessage("Illegal port for backend server, use only numbers");
|
||||
}
|
||||
|
||||
void _showMissingPortError() {
|
||||
showMessage("Missing port for lawin server");
|
||||
showMessage("Missing port for backend server");
|
||||
}
|
||||
|
||||
void _showMissingHostError() {
|
||||
showMessage("Missing the host name for lawin server");
|
||||
showMessage("Missing the host name for backend server");
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> downloadServerInteractive(bool closeAutomatically) async {
|
||||
var download = compute(downloadServer, true);
|
||||
return await showDialog<bool>(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
FutureBuilderDialog(
|
||||
future: download,
|
||||
loadingMessage: "Downloading server...",
|
||||
loadedBody: FutureBuilderDialog.ofMessage(
|
||||
"The server was downloaded successfully"),
|
||||
errorMessageBuilder: (
|
||||
message) => "Cannot download server: $message",
|
||||
closeAutomatically: closeAutomatically
|
||||
)
|
||||
) ?? download.isCompleted();
|
||||
}
|
||||
333
lib/src/embedded/auth.dart
Normal file
333
lib/src/embedded/auth.dart
Normal file
@@ -0,0 +1,333 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
|
||||
final Directory _profiles = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\profiles");
|
||||
|
||||
const String _token = "reboot_token";
|
||||
const String _clientId = "reboot_client";
|
||||
const String _device = "reboot_device";
|
||||
const String _sessionId = "3c3662bcb661d6de679c636744c66b62";
|
||||
|
||||
List<Map<String, Object>> getAccounts(Context context) {
|
||||
return context.query.getList("accountId").map(getAccount).toList();
|
||||
}
|
||||
|
||||
Map<String, Object> getAccount(String account) {
|
||||
return {"id": account, "displayName": _parseUsername(account), "externalAuths": {}};
|
||||
}
|
||||
|
||||
Map<String, Object> getAccountInfo(Context context) {
|
||||
var usernameId = context.pathParams.get("accountId")!;
|
||||
var accountName = _parseUsername(usernameId);
|
||||
return {
|
||||
"id": usernameId,
|
||||
"displayName": accountName,
|
||||
"name": "Reboot",
|
||||
"email": usernameId,
|
||||
"failedLoginAttempts": 0,
|
||||
"lastLogin": "2022-11-08T18:55:52.341Z",
|
||||
"numberOfDisplayNameChanges": 0,
|
||||
"ageGroup": "UNKNOWN",
|
||||
"headless": false,
|
||||
"country": "US",
|
||||
"lastName": "Server",
|
||||
"preferredLanguage": "en",
|
||||
"canUpdateDisplayName": false,
|
||||
"tfaEnabled": false,
|
||||
"emailVerified": true,
|
||||
"minorVerified": false,
|
||||
"minorExpected": false,
|
||||
"minorStatus": "UNKNOWN"
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getExternalAuths(Context context) => [];
|
||||
|
||||
Future<Map<String, Object>> getOAuthToken(Context context) async {
|
||||
var usernameId = await _getUsername(context);
|
||||
var accountName = _parseUsername(usernameId);
|
||||
return {
|
||||
"access_token": _token,
|
||||
"expires_in": 28800,
|
||||
"expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"token_type": "bearer",
|
||||
"refresh_token": _token,
|
||||
"refresh_expires": 86400,
|
||||
"refresh_expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"account_id": usernameId,
|
||||
"client_id": _clientId,
|
||||
"internal_client": true,
|
||||
"client_service": "fortnite",
|
||||
"displayName": accountName,
|
||||
"app": "fortnite",
|
||||
"in_app_id": usernameId,
|
||||
"device_id": _device
|
||||
};
|
||||
}
|
||||
|
||||
Future<String> _getUsername(Context context) async {
|
||||
var params = await parseBody(context);
|
||||
var username = params["username"];
|
||||
return username ?? "unknown@projectreboot.dev";
|
||||
}
|
||||
|
||||
Map<String, Object> verifyOAuthToken(Context context) {
|
||||
return {
|
||||
"token": _token,
|
||||
"session_id": _sessionId,
|
||||
"token_type": "bearer",
|
||||
"client_id": _clientId,
|
||||
"internal_client": true,
|
||||
"client_service": "fortnite",
|
||||
"account_id": "unknown",
|
||||
"expires_in": 28800,
|
||||
"expires_at": "9999-12-02T01:12:01.100Z",
|
||||
"auth_method": "exchange_code",
|
||||
"display_name": "unknown",
|
||||
"app": "fortnite",
|
||||
"in_app_id": "unknown",
|
||||
"device_id": _device
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getExchange(Context context) => [];
|
||||
|
||||
List<String> getSsoDomains(Context context) => [
|
||||
"unrealengine.com",
|
||||
"unrealtournament.com",
|
||||
"fortnite.com",
|
||||
"epicgames.com"
|
||||
];
|
||||
|
||||
String tryPlayOnPlatform(Context context) => "true";
|
||||
|
||||
List<Map<String, Object>> getFeatures(Context context) => [];
|
||||
|
||||
Map<String, Object?> getProfile(Context context){
|
||||
var profileId = context.query.get("profileId");
|
||||
if (profileId == null) {
|
||||
return {"Error": "Profile not defined."};
|
||||
}
|
||||
|
||||
var profileJson = _getProfileJson(profileId, context);
|
||||
var profileFile = _getProfileFile(context);
|
||||
var baseRevision = profileJson["rvn"] ?? 0;
|
||||
var queryRevision = context.query.getInt("rvn") ?? -1;
|
||||
var profileChanges = _getFullProfileUpdate(context, profileId, profileJson, queryRevision, baseRevision);
|
||||
if(profileId == "athena" && !profileFile.existsSync()) {
|
||||
profileFile.writeAsStringSync(json.encode(profileJson), flush: true);
|
||||
}
|
||||
|
||||
return {
|
||||
"profileRevision": baseRevision,
|
||||
"profileId": profileId,
|
||||
"profileChangesBaseRevision": baseRevision,
|
||||
"profileChanges": profileChanges,
|
||||
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
|
||||
"serverTime": "2022-11-08T18:55:52.341Z",
|
||||
"responseVersion": 1
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getProfileJson(String profileId, Context context) {
|
||||
if(profileId == "athena"){
|
||||
var profile = _getProfileFile(context);
|
||||
if(profile.existsSync()){
|
||||
return json.decode(profile.readAsStringSync());
|
||||
}
|
||||
|
||||
var body = loadEmbedded("profiles/$profileId.json").readAsStringSync();
|
||||
return json.decode(body);
|
||||
}
|
||||
|
||||
var profileJson = json.decode(loadEmbedded("profiles/$profileId.json").readAsStringSync());
|
||||
return profileJson;
|
||||
}
|
||||
|
||||
Future<Map<String, Object>> equipItem(Context context) async {
|
||||
var profileFile = _getProfileFile(context);
|
||||
var profileJson = json.decode(profileFile.readAsStringSync());
|
||||
var baseRevision = profileJson["rvn"] ?? 0;
|
||||
var queryRevision = context.query.getInt("rvn") ?? -1;
|
||||
|
||||
var body = json.decode(utf8.decode(await context.body));
|
||||
var variant = _getReturnVariant(body, profileJson);
|
||||
var change = _getStatsChanged(body, profileJson);
|
||||
var profileChanges = _getProfileChanges(queryRevision, baseRevision, profileJson, change, body, variant);
|
||||
profileFile.writeAsStringSync(json.encode(profileJson));
|
||||
return {
|
||||
"profileRevision": baseRevision,
|
||||
"profileId": "athena",
|
||||
"profileChangesBaseRevision": baseRevision,
|
||||
"profileChanges": profileChanges,
|
||||
"profileCommandRevision": profileJson["commandRevision"] ?? 0,
|
||||
"serverTime": "2022-11-08T18:55:52.341Z",
|
||||
"responseVersion": 1
|
||||
};
|
||||
}
|
||||
|
||||
List<dynamic> _getProfileChanges(int queryRevision, baseRevision, profileJson, bool change, body, bool variant) {
|
||||
var changes = [];
|
||||
if (change) {
|
||||
var category = ("favorite_${body["slotName"] ?? "character"}")
|
||||
.toLowerCase();
|
||||
if (category == "favorite_itemwrap") {
|
||||
category += "s";
|
||||
}
|
||||
|
||||
profileJson["rvn"] = (profileJson["rvn"] ?? 0) + 1;
|
||||
profileJson["commandRevision"] = (profileJson["commandRevision"] ?? 0) + 1;
|
||||
|
||||
changes.add({
|
||||
"changeType": "statModified",
|
||||
"name": category,
|
||||
"value": profileJson["stats"]["attributes"][category]
|
||||
});
|
||||
if (variant) {
|
||||
changes.add({
|
||||
"changeType": "itemAttrChanged",
|
||||
"itemId": body["itemToSlot"],
|
||||
"attributeName": "variants",
|
||||
"attributeValue": profileJson["items"][body["itemToSlot"]]["attributes"]["variants"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(queryRevision != baseRevision){
|
||||
return [{
|
||||
"changeType": "fullProfileUpdate",
|
||||
"profile": profileJson
|
||||
}];
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
bool _getStatsChanged(body, profileJson) {
|
||||
var slotName = body["slotName"];
|
||||
if (slotName == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (slotName) {
|
||||
case "Character":
|
||||
profileJson["stats"]["attributes"]["favorite_character"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Backpack":
|
||||
profileJson["stats"]["attributes"]["favorite_backpack"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Pickaxe":
|
||||
profileJson["stats"]["attributes"]["favorite_pickaxe"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Glider":
|
||||
profileJson["stats"]["attributes"]["favorite_glider"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "SkyDiveContrail":
|
||||
profileJson["stats"]["attributes"]["favorite_skydivecontrail"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "MusicPack":
|
||||
profileJson["stats"]["attributes"]["favorite_musicpack"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "LoadingScreen":
|
||||
profileJson["stats"]["attributes"]["favorite_loadingscreen"] =
|
||||
body["itemToSlot"] ?? "";
|
||||
return true;
|
||||
case "Dance":
|
||||
var index = body["indexWithinSlot"] ?? 0;
|
||||
if (index >= 0) {
|
||||
profileJson["stats"]["attributes"]["favorite_dance"][index] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
|
||||
return true;
|
||||
case "ItemWrap":
|
||||
var index = body["indexWithinSlot"] ?? 0;
|
||||
if (index < 0) {
|
||||
for (var i = 0; i < 7; i++) {
|
||||
profileJson["stats"]["attributes"]["favorite_itemwraps"][i] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
} else {
|
||||
profileJson["stats"]["attributes"]["favorite_itemwraps"][index] =
|
||||
body["itemToSlot"] ?? "";
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _getReturnVariant(body, profileJson) {
|
||||
var variantUpdates = body["variantUpdates"] ?? [];
|
||||
if(!variantUpdates.toString().contains("active")){
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
var variantJson = profileJson["items"][body["itemToSlot"]]["attributes"]["variants"] ?? [];
|
||||
if (variantJson.isEmpty) {
|
||||
variantJson = variantUpdates;
|
||||
}
|
||||
|
||||
for (var i in variantJson) {
|
||||
try {
|
||||
if (variantJson[i]["channel"].toLowerCase() == body["variantUpdates"][i]["channel"].toLowerCase()) {
|
||||
profileJson["items"][body["itemToSlot"]]["attributes"]["variants"][i]["active"] = body["variantUpdates"][i]["active"] ?? "";
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
// Ignored
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> _getFullProfileUpdate(Context context, String profileName, Map<String, dynamic> profileJson, int queryRevision, int baseRevision) {
|
||||
if (queryRevision == baseRevision) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (profileName == "athena") {
|
||||
var season = parseSeason(context);
|
||||
profileJson["stats"]["attributes"]["season_num"] = season;
|
||||
profileJson["stats"]["attributes"]["book_purchased"] = true;
|
||||
profileJson["stats"]["attributes"]["book_level"] = 100;
|
||||
profileJson["stats"]["attributes"]["season_match_boost"] = 100;
|
||||
profileJson["stats"]["attributes"]["season_friend_match_boost"] = 100;
|
||||
}
|
||||
|
||||
return [{
|
||||
"changeType": "fullProfileUpdate",
|
||||
"profile": profileJson
|
||||
}];
|
||||
}
|
||||
|
||||
String _parseUsername(String username) =>
|
||||
username.contains("@") ? username.split("@")[0] : username;
|
||||
|
||||
File _getProfileFile(Context context) {
|
||||
if(!_profiles.existsSync()){
|
||||
_profiles.createSync(recursive: true);
|
||||
}
|
||||
|
||||
return File("${_profiles.path}\\ClientProfile-${parseSeasonBuild(context)}.json");
|
||||
}
|
||||
|
||||
36
lib/src/embedded/error.dart
Normal file
36
lib/src/embedded/error.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
class EmbeddedErrorWriter extends ErrorWriter {
|
||||
static const String _errorName = "errors.com.lawinserver.common.not_found";
|
||||
static const String _errorCode = "1004";
|
||||
|
||||
@override
|
||||
FutureOr<Response> make404(Context ctx) {
|
||||
stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}");
|
||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||
return Response.json(
|
||||
statusCode: 204,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
|
||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||
return Response(
|
||||
statusCode: 500,
|
||||
body: {
|
||||
"errorCode": _errorName,
|
||||
"errorMessage": "Sorry the resource you were trying to find could not be found",
|
||||
"numericErrorCode": _errorCode,
|
||||
"originatingService": "any",
|
||||
"intent": "prod"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
33
lib/src/embedded/lightswitch.dart
Normal file
33
lib/src/embedded/lightswitch.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
Map<String, Object?> getFortniteStatus(Context context) => {
|
||||
"serviceInstanceId": "fortnite",
|
||||
"status": "UP",
|
||||
"message": "Fortnite is online",
|
||||
"maintenanceUri": null,
|
||||
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
|
||||
"allowedActions": [],
|
||||
"banned": false,
|
||||
"launcherInfoDTO": {
|
||||
"appName": "Fortnite",
|
||||
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
|
||||
"namespace": "fn"
|
||||
}
|
||||
};
|
||||
|
||||
List<Map<String, Object?>> getBulkStatus(Context context) => [
|
||||
{
|
||||
"serviceInstanceId": "fortnite",
|
||||
"status": "UP",
|
||||
"message": "fortnite is up.",
|
||||
"maintenanceUri": null,
|
||||
"overrideCatalogIds": ["a7f138b2e51945ffbfdacc1af0541053"],
|
||||
"allowedActions": ["PLAY", "DOWNLOAD"],
|
||||
"banned": false,
|
||||
"launcherInfoDTO": {
|
||||
"appName": "Fortnite",
|
||||
"catalogItemId": "4fe75bbc5a674f4f9b356b5c90567da5",
|
||||
"namespace": "fn"
|
||||
}
|
||||
}
|
||||
];
|
||||
144
lib/src/embedded/matchmaking.dart
Normal file
144
lib/src/embedded/matchmaking.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
|
||||
String _build = "0";
|
||||
String? _customIp;
|
||||
|
||||
Map<String, Object> getPlayerTicket(Context context){
|
||||
var bucketId = context.query.get("bucketId");
|
||||
if(bucketId == null){
|
||||
return {"Error": "Missing bucket id"};
|
||||
}
|
||||
|
||||
_build = bucketId.split(":")[0];
|
||||
_customIp = context.query.get("player.option.customKey");
|
||||
return {
|
||||
"serviceUrl": "ws://127.0.0.1:8080",
|
||||
"ticketType": "mms-player",
|
||||
"payload": "69=",
|
||||
"signature": "420="
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, Object?> getSessionAccount(Context context) => {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"sessionId": context.pathParams.get("sessionId"),
|
||||
"key": "AOJEv8uTFmUh7XM2328kq9rlAzeQ5xzWzPIiyKn2s7s="
|
||||
};
|
||||
|
||||
Future<Map<String, Object?>> getMatch(Context context, String Function() ipQuery) async {
|
||||
var ipAndPort = _customIp ?? ipQuery().trim();
|
||||
var ip = ipAndPort.contains(":") ? ipAndPort.split(":")[0] : ipAndPort;
|
||||
var port = ipAndPort.contains(":") ? int.parse(ipAndPort.split(":")[1]) : 7777;
|
||||
return {
|
||||
"id": context.pathParams.get("sessionId"),
|
||||
"ownerId": _randomUUID(),
|
||||
"ownerName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
|
||||
"serverName": "[DS]fortnite-liveeugcec1c2e30ubrcore0a-z8hj-1968",
|
||||
"serverAddress": ip,
|
||||
"serverPort": port,
|
||||
"maxPublicPlayers": 220,
|
||||
"openPublicPlayers": 175,
|
||||
"maxPrivatePlayers": 0,
|
||||
"openPrivatePlayers": 0,
|
||||
"attributes": {
|
||||
"REGION_s": "EU",
|
||||
"GAMEMODE_s": "FORTATHENA",
|
||||
"ALLOWBROADCASTING_b": true,
|
||||
"SUBREGION_s": "GB",
|
||||
"DCID_s": "FORTNITE-LIVEEUGCEC1C2E30UBRCORE0A-14840880",
|
||||
"tenant_s": "Fortnite",
|
||||
"MATCHMAKINGPOOL_s": "Any",
|
||||
"STORMSHIELDDEFENSETYPE_i": 0,
|
||||
"HOTFIXVERSION_i": 0,
|
||||
"PLAYLISTNAME_s": "Playlist_DefaultSolo",
|
||||
"SESSIONKEY_s": _randomUUID(),
|
||||
"TENANT_s": "Fortnite",
|
||||
"BEACONPORT_i": 15009
|
||||
},
|
||||
"publicPlayers": [],
|
||||
"privatePlayers": [],
|
||||
"totalPlayers": 45,
|
||||
"allowJoinInProgress": false,
|
||||
"shouldAdvertise": false,
|
||||
"isDedicated": false,
|
||||
"usesStats": false,
|
||||
"allowInvites": false,
|
||||
"usesPresence": false,
|
||||
"allowJoinViaPresence": true,
|
||||
"allowJoinViaPresenceFriendsOnly": false,
|
||||
"buildUniqueId": _build,
|
||||
"lastUpdated": "2022-11-08T18:55:52.341Z",
|
||||
"started": false
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, Object>> getMatchmakingRequests() => [];
|
||||
|
||||
void queueMatchmaking(WebSocket ws) {
|
||||
var now = DateTime.now();
|
||||
var ticketId = md5.convert(utf8.encode("1$now")).toString();
|
||||
var matchId = md5.convert(utf8.encode("2$now")).toString();
|
||||
var sessionId = md5.convert(utf8.encode("3$now")).toString();
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"state": "Connecting"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"totalPlayers": 1,
|
||||
"connectedPlayers": 1,
|
||||
"state": "Waiting"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"ticketId": ticketId,
|
||||
"queuedPlayers": 0,
|
||||
"estimatedWaitSec": 0,
|
||||
"status": {},
|
||||
"state": "Queued"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"matchId": matchId,
|
||||
"state": "SessionAssignment"
|
||||
},
|
||||
"name": "StatusUpdate"
|
||||
})
|
||||
));
|
||||
|
||||
ws.addUtf8Text(utf8.encode(
|
||||
jsonEncode({
|
||||
"payload": {
|
||||
"matchId": matchId,
|
||||
"sessionId": sessionId,
|
||||
"joinDelaySec": 1
|
||||
},
|
||||
"name": "Play"
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
String _randomUUID() => const Uuid().v4().replaceAll("-", "").toUpperCase();
|
||||
1205
lib/src/embedded/misc.dart
Normal file
1205
lib/src/embedded/misc.dart
Normal file
File diff suppressed because it is too large
Load Diff
16
lib/src/embedded/privacy.dart
Normal file
16
lib/src/embedded/privacy.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
Map<String, Object?> getPrivacy(Context context) => {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"optOutOfPublicLeaderboards": false
|
||||
};
|
||||
|
||||
|
||||
Future<Map<String, Object?>> postPrivacy(Context context) async {
|
||||
var body = await parseBody(context);
|
||||
return {
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"optOutOfPublicLeaderboards": body["optOutOfPublicLeaderboards"]
|
||||
};
|
||||
}
|
||||
138
lib/src/embedded/server.dart
Normal file
138
lib/src/embedded/server.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import "package:jaguar/jaguar.dart";
|
||||
import "package:reboot_launcher/src/embedded/auth.dart";
|
||||
import 'package:reboot_launcher/src/embedded/misc.dart';
|
||||
import 'package:reboot_launcher/src/embedded/privacy.dart';
|
||||
import "package:reboot_launcher/src/embedded/storage.dart";
|
||||
import 'package:reboot_launcher/src/embedded/storefront.dart';
|
||||
import "package:reboot_launcher/src/embedded/version.dart";
|
||||
|
||||
import '../util/server.dart';
|
||||
import "error.dart";
|
||||
import "lightswitch.dart";
|
||||
import 'matchmaking.dart';
|
||||
|
||||
bool _loggingCapabilities = false;
|
||||
|
||||
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
|
||||
var server = _createServer(ipQuery);
|
||||
await server.serve(logRequests: true);
|
||||
return server;
|
||||
}
|
||||
|
||||
Future<Jaguar> startEmbeddedMatchmaker() async {
|
||||
var server = _createMatchmaker();
|
||||
server.serve(logRequests: true);
|
||||
return server;
|
||||
}
|
||||
|
||||
Jaguar _createServer(String Function() ipQuery) {
|
||||
var server = Jaguar(address: "127.0.0.1", port: 3551, errorWriter: EmbeddedErrorWriter());
|
||||
|
||||
// Version
|
||||
server.getJson("/fortnite/api/version", getVersion);
|
||||
server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate);
|
||||
server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate);
|
||||
server.getJson("/fortnite/api/versioncheck*", hasUpdate);
|
||||
|
||||
// Auth
|
||||
server.getJson("/account/api/public/account/displayName/:accountId", getAccountInfo);
|
||||
server.getJson("/account/api/public/account/:accountId", getAccountInfo);
|
||||
server.getJson("/account/api/public/account/:accountId/externalAuths", getExternalAuths);
|
||||
server.getJson("/account/api/public/account", getAccounts);
|
||||
server.delete("/account/api/oauth/sessions/kill/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/account/api/oauth/verify", verifyOAuthToken);
|
||||
server.postJson("/account/api/oauth/token", getOAuthToken);
|
||||
server.postJson("/account/api/oauth/exchange", getExchange);
|
||||
server.getJson("/account/api/epicdomains/ssodomains", getSsoDomains);
|
||||
server.post("/fortnite/api/game/v2/tryPlayOnPlatform/account/*", tryPlayOnPlatform);
|
||||
server.post("/datarouter/api/v1/public/data/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/fortnite/api/game/v2/enabled_features", getFeatures);
|
||||
server.postJson("/fortnite/api/game/v2/grant_access/*", (context) => Response(statusCode: 204));
|
||||
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/EquipBattleRoyaleCustomization", equipItem);
|
||||
server.postJson("/fortnite/api/game/v2/profile/:profileId/client/*", getProfile);
|
||||
|
||||
// Storage
|
||||
server.getJson("/fortnite/api/cloudstorage/system", getStorageSettings);
|
||||
server.get("/fortnite/api/cloudstorage/system/:file", getStorageSetting);
|
||||
server.getJson("/fortnite/api/cloudstorage/user/:accountId", getStorageAccount);
|
||||
server.getJson("/fortnite/api/cloudstorage/user/:accountId/:file", getStorageFile);
|
||||
server.put("/fortnite/api/cloudstorage/user/:accountId/:file", addStorageFile);
|
||||
|
||||
// Status
|
||||
server.getJson("/lightswitch/api/service/Fortnite/status", getFortniteStatus);
|
||||
server.getJson("/lightswitch/api/service/bulk/status", getBulkStatus);
|
||||
|
||||
// Keychain and catalog
|
||||
server.get("/fortnite/api/storefront/v2/catalog", getCatalog);
|
||||
server.get("/fortnite/api/storefront/v2/keychain", getKeyChain);
|
||||
server.get("/catalog/api/shared/bulk/offers", getOffers);
|
||||
|
||||
// Matchmaking
|
||||
server.get("/fortnite/api/matchmaking/session/findPlayer/*", (context) => Response(statusCode: 200));
|
||||
server.getJson("/fortnite/api/game/v2/matchmakingservice/ticket/player/*", getPlayerTicket);
|
||||
server.getJson("/fortnite/api/game/v2/matchmaking/account/:accountId/session/:sessionId", getSessionAccount);
|
||||
server.getJson("/fortnite/api/matchmaking/session/:sessionId", (context) => getMatch(context, ipQuery));
|
||||
server.post("/fortnite/api/matchmaking/session/:accountId/join", (context) => Response(statusCode: 204));
|
||||
server.postJson("/fortnite/api/matchmaking/session/matchMakingRequest", (context) => getMatchmakingRequests);
|
||||
|
||||
// Misc
|
||||
server.getJson("/api/v1/events/Fortnite/download/*", getDownload);
|
||||
server.getJson("/fortnite/api/receipts/v1/account/:accountId/receipts", getReceipts);
|
||||
server.getJson("/content/api/pages/*", getContentPages);
|
||||
server.getJson("/friends/api/v1/:accountId/settings", getFriendsSettings);
|
||||
server.getJson("/friends/api/v1/:accountId/blocklist", getFriendsBlocklist);
|
||||
server.getJson("/friends/api/public/blocklist/:accountId", getFriendsBlocklist);
|
||||
server.getJson("/friends/api/public/friends/:accountId", getFriendsList);
|
||||
server.getJson("/friends/api/public/list/fortnite/:accountId/recentPlayers", getRecentPlayers);
|
||||
server.getJson("/fortnite/api/calendar/v1/timeline", getTimeline);
|
||||
server.getJson("/fortnite/api/game/v2/events/tournamentandhistory/:accountId/EU/WindowsClient", getTournamentHistory);
|
||||
server.get("/waitingroom/api/waitingroom", (context) => Response(statusCode: 204));
|
||||
server.postJson("/api/v1/user/setting", (context) => []);
|
||||
server.getJson("/eulatracking/api/public/agreements/fn/account/*", (context) => Response(statusCode: 204));
|
||||
server.getJson("/socialban/api/public/v1/:accountId", getSocialBan);
|
||||
server.getJson("/party/api/v1/Fortnite/user/*", getParty);
|
||||
server.getJson("/friends/api/v1/*/settings", (context) => {});
|
||||
server.getJson("/friends/api/v1/*/blocklist", (context) => {});
|
||||
server.getJson("/friends/api/public/friends", (context) => []);
|
||||
server.getJson("/friends/api/v1/:accountId/summary", (context) => []);
|
||||
server.getJson("/friends/api/public/list/fortnite/*/recentPlayers", (context) => []);
|
||||
server.getJson("/friends/api/public/blocklist/*", getBlockedFriends);
|
||||
|
||||
// Privacy
|
||||
server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy);
|
||||
server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
|
||||
|
||||
return _addLoggingCapabilities(server);
|
||||
}
|
||||
Jaguar _createMatchmaker(){
|
||||
var server = Jaguar(address: "127.0.0.1", port: 8080);
|
||||
WebSocket? ws;
|
||||
server.wsStream(
|
||||
"/",
|
||||
(_, input) => ws = input,
|
||||
after: [(_) => queueMatchmaking(ws!)]
|
||||
);
|
||||
return _addLoggingCapabilities(server);
|
||||
}
|
||||
|
||||
Jaguar _addLoggingCapabilities(Jaguar server) {
|
||||
if(_loggingCapabilities){
|
||||
return server;
|
||||
}
|
||||
|
||||
server.log.onRecord.listen((line) {
|
||||
stdout.writeln(line);
|
||||
serverLogFile.writeAsString("$line\n", mode: FileMode.append);
|
||||
});
|
||||
|
||||
server.onException.add((ctx, exception, trace) {
|
||||
stderr.writeln("An error occurred: $exception");
|
||||
serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append);
|
||||
});
|
||||
|
||||
_loggingCapabilities = true;
|
||||
return server;
|
||||
}
|
||||
111
lib/src/embedded/storage.dart
Normal file
111
lib/src/embedded/storage.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:jaguar/jaguar.dart';
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:reboot_launcher/src/embedded/utils.dart';
|
||||
|
||||
import '../util/os.dart';
|
||||
|
||||
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
|
||||
|
||||
const String _engineName = "DefaultEngine.ini";
|
||||
final String _engineIni = loadEmbedded("config/$_engineName").readAsStringSync();
|
||||
|
||||
const String _gameName = "DefaultGame.ini";
|
||||
final String _gameIni = loadEmbedded("config/$_gameName").readAsStringSync();
|
||||
|
||||
const String _runtimeName = "DefaultRuntimeOptions.ini";
|
||||
final String _runtimeIni = loadEmbedded("config/$_runtimeName").readAsStringSync();
|
||||
|
||||
List<Map<String, Object>> getStorageSettings(Context context) => [
|
||||
_getStorageSetting(_engineName, _engineIni),
|
||||
_getStorageSetting(_gameName, _gameIni),
|
||||
_getStorageSetting(_runtimeName, _runtimeIni)
|
||||
];
|
||||
|
||||
Map<String, Object> _getStorageSetting(String name, String source){
|
||||
var bytes = utf8.encode(source);
|
||||
return {
|
||||
"uniqueFilename": name,
|
||||
"filename": name,
|
||||
"hash": sha1.convert(bytes).toString(),
|
||||
"hash256": sha256.convert(bytes).toString(),
|
||||
"length": bytes.length,
|
||||
"contentType": "application/octet-stream",
|
||||
"uploaded": "2020-02-23T18:35:53.967Z",
|
||||
"storageType": "S3",
|
||||
"storageIds": {},
|
||||
"doNotCache": true
|
||||
};
|
||||
}
|
||||
|
||||
Response getStorageSetting(Context context) {
|
||||
switch(context.pathParams.get("file")){
|
||||
case _engineName:
|
||||
return Response(body: _engineIni);
|
||||
case _gameName:
|
||||
return Response(body: _gameIni);
|
||||
case _runtimeName:
|
||||
return Response(body: _runtimeIni);
|
||||
default:
|
||||
return Response();
|
||||
}
|
||||
}
|
||||
|
||||
Response getStorageFile(Context context) {
|
||||
if (context.pathParams.get("file")?.toLowerCase() != "clientsettings.sav") {
|
||||
return Response.json(
|
||||
{"error": "File not found"},
|
||||
statusCode: 404
|
||||
);
|
||||
}
|
||||
|
||||
var file = _getSettingsFile(context);
|
||||
return Response(
|
||||
body: file.existsSync() ? file.readAsBytesSync() : null,
|
||||
headers: {"content-type": "application/octet-stream"}
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> getStorageAccount(Context context) {
|
||||
var file = _getSettingsFile(context);
|
||||
if (!file.existsSync()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var content = file.readAsBytesSync();
|
||||
return [{
|
||||
"uniqueFilename": "ClientSettings.Sav",
|
||||
"filename": "ClientSettings.Sav",
|
||||
"hash": sha1.convert(content).toString(),
|
||||
"hash256": sha256.convert(content).toString(),
|
||||
"length": content.length,
|
||||
"contentType": "application/octet-stream",
|
||||
"uploaded": "2020-02-23T18:35:53.967Z",
|
||||
"storageType": "S3",
|
||||
"storageIds": {},
|
||||
"accountId": context.pathParams.get("accountId"),
|
||||
"doNotCache": true
|
||||
}];
|
||||
}
|
||||
|
||||
Future<Response> addStorageFile(Context context) async {
|
||||
if(!_settings.existsSync()){
|
||||
await _settings.create(recursive: true);
|
||||
}
|
||||
|
||||
var file = _getSettingsFile(context);
|
||||
await file.writeAsBytes(await context.body);
|
||||
return Response(statusCode: 204);
|
||||
}
|
||||
|
||||
File _getSettingsFile(Context context) {
|
||||
if(!_settings.existsSync()){
|
||||
_settings.createSync(recursive: true);
|
||||
}
|
||||
|
||||
return File("${_settings.path}\\ClientSettings-${parseSeasonBuild(context)}.Sav");
|
||||
}
|
||||
19
lib/src/embedded/storefront.dart
Normal file
19
lib/src/embedded/storefront.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:jaguar/http/response/response.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
|
||||
final String _keyChain = loadEmbedded("responses/keychain.json").readAsStringSync();
|
||||
final String _catalog = loadEmbedded("responses/catalog.json").readAsStringSync();
|
||||
|
||||
Response getCatalog(Context context) {
|
||||
if (context.headers.value("user-agent")?.contains("2870186") == true) {
|
||||
return Response(statusCode: 404);
|
||||
}
|
||||
|
||||
return Response(body: _catalog, headers: {"content-type": "application/json"});
|
||||
}
|
||||
|
||||
Response getKeyChain(Context context) => Response(body: _keyChain, headers: {"content-type": "application/json"});
|
||||
|
||||
Map<String, Object> getOffers(Context context) => {};
|
||||
42
lib/src/embedded/utils.dart
Normal file
42
lib/src/embedded/utils.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
|
||||
const String _chars =
|
||||
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
|
||||
final Random _random = Random();
|
||||
|
||||
String randomString(int length) => String.fromCharCodes(
|
||||
Iterable.generate(length, (_) => _chars.codeUnitAt(_random.nextInt(_chars.length))));
|
||||
|
||||
double parseSeasonBuild(Context context){
|
||||
String? userAgent = context.headers.value("user-agent");
|
||||
if (userAgent == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
try {
|
||||
var build = userAgent.split("Release-")[1].split("-")[0];
|
||||
if (build.split(".").length == 3) {
|
||||
var value = build.split(".");
|
||||
return double.parse("${value[0]}.${value[1]}${value[2]}");
|
||||
}
|
||||
|
||||
return double.parse(build);
|
||||
} catch (_) {
|
||||
return 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
int parseSeason(Context context) => int.parse(parseSeasonBuild(context).toString().split(".")[0]);
|
||||
|
||||
Future<HashMap<String, String?>> parseBody(Context context) async {
|
||||
var params = HashMap<String, String?>();
|
||||
utf8.decode(await context.req.body)
|
||||
.split("&")
|
||||
.map((entry) => MapEntry(entry.substring(0, entry.indexOf("=")), entry.substring(entry.indexOf("=") + 1)))
|
||||
.forEach((element) => params[element.key] = Uri.decodeQueryComponent(element.value));
|
||||
return params;
|
||||
}
|
||||
41
lib/src/embedded/version.dart
Normal file
41
lib/src/embedded/version.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:jaguar/http/context/context.dart';
|
||||
import 'package:reboot_launcher/src/util/time.dart';
|
||||
|
||||
Map<String, Object> getVersion(Context context) => {
|
||||
"app": "fortnite",
|
||||
"serverDate": "2022-11-08T18:55:52.341Z",
|
||||
"overridePropertiesVersion": "unknown",
|
||||
"cln": "17951730",
|
||||
"build": "444",
|
||||
"moduleName": "Fortnite-Core",
|
||||
"buildDate": "2021-10-27T21:00:51.697Z",
|
||||
"version": "18.30",
|
||||
"branch": "Release-18.30",
|
||||
"modules": {
|
||||
"Epic-LightSwitch-AccessControlCore": {
|
||||
"cln": "17237679",
|
||||
"build": "b2130",
|
||||
"buildDate": "2021-08-19T18:56:08.144Z",
|
||||
"version": "1.0.0",
|
||||
"branch": "trunk"
|
||||
},
|
||||
"epic-xmpp-api-v1-base": {
|
||||
"cln": "5131a23c1470acbd9c94fae695ef7d899c1a41d6",
|
||||
"build": "b3595",
|
||||
"buildDate": "2019-07-30T09:11:06.587Z",
|
||||
"version": "0.0.1",
|
||||
"branch": "master"
|
||||
},
|
||||
"epic-common-core": {
|
||||
"cln": "17909521",
|
||||
"build": "3217",
|
||||
"buildDate": "2021-10-25T18:41:12.486Z",
|
||||
"version": "3.0",
|
||||
"branch": "TRUNK"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Map<String, Object> hasUpdate(Context context) => {
|
||||
"type": "NO_UPDATE"
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import 'package:version/version.dart';
|
||||
class FortniteBuild {
|
||||
final Version version;
|
||||
final String link;
|
||||
final bool hasManifest;
|
||||
|
||||
FortniteBuild({required this.version, required this.link, required this.hasManifest});
|
||||
FortniteBuild({required this.version, required this.link});
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ import 'package:path/path.dart' as path;
|
||||
class FortniteVersion {
|
||||
String name;
|
||||
Directory location;
|
||||
bool memoryFix;
|
||||
|
||||
FortniteVersion.fromJson(json)
|
||||
: name = json["name"],
|
||||
location = Directory(json["location"]),
|
||||
memoryFix = json["memory_fix"] ?? false;
|
||||
location = Directory(json["location"]);
|
||||
|
||||
FortniteVersion({required this.name, required this.location, required this.memoryFix});
|
||||
FortniteVersion({required this.name, required this.location});
|
||||
|
||||
static File? findExecutable(Directory directory, String name) {
|
||||
try{
|
||||
@@ -32,13 +30,17 @@ class FortniteVersion {
|
||||
return findExecutable(location, "FortniteLauncher.exe");
|
||||
}
|
||||
|
||||
File? get eacExecutable {
|
||||
return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'location': location.path,
|
||||
};
|
||||
'name': name,
|
||||
'location': location.path
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FortniteVersion{name: $name, location: $location}';
|
||||
return 'FortniteVersion{name: $name, location: $location';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart' hide WindowBorder;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/page/settings_page.dart';
|
||||
import 'package:reboot_launcher/src/page/launcher_page.dart';
|
||||
import 'package:reboot_launcher/src/page/server_page.dart';
|
||||
@@ -9,6 +17,7 @@ import 'package:reboot_launcher/src/widget/os/window_border.dart';
|
||||
import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import 'info_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -20,117 +29,294 @@ class HomePage extends StatefulWidget {
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener {
|
||||
static const double _headerSize = 48.0;
|
||||
static const double _sectionSize = 94.0;
|
||||
static const double _sectionSize = 100.0;
|
||||
static const double _defaultPadding = 12.0;
|
||||
static const double _openMenuSize = 320.0;
|
||||
static const int _headerButtonCount = 3;
|
||||
static const int _sectionButtonCount = 4;
|
||||
|
||||
bool _focused = true;
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
|
||||
final GlobalKey _searchKey = GlobalKey();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
|
||||
final RxBool _focused = RxBool(true);
|
||||
final RxInt _index = RxInt(0);
|
||||
|
||||
bool _shouldMaximize = false;
|
||||
int _index = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
_searchController.addListener(() {
|
||||
if (searchValue.isEmpty) {
|
||||
_searchItems.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_searchItems.value = _allItems.whereType<PaneItem>()
|
||||
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
|
||||
.toList()
|
||||
.cast<NavigationPaneItem>();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
windowManager.removeListener(this);
|
||||
_searchFocusNode.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowFocus() {
|
||||
setState(() => _focused = true);
|
||||
_focused.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
setState(() => _focused = !_focused);
|
||||
_focused.value = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
void onWindowMoved() {
|
||||
_settingsController.saveWindowOffset(appWindow.position);
|
||||
super.onWindowMoved();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
if(!_gameController.started() || !_serverController.started()) {
|
||||
windowManager.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return InfoDialog(
|
||||
text: "Closing the launcher while a backend is running may make the game not work correctly. Are you sure you want to proceed?",
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary,
|
||||
text: "Don't close",
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
type: ButtonType.primary,
|
||||
onTap: () => windowManager.destroy(),
|
||||
text: "Close",
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) => _calculateSize(),
|
||||
child: SizeChangedLayoutNotifier(
|
||||
child: Obx(() => Stack(
|
||||
children: [
|
||||
_createNavigationView(),
|
||||
_createTitleBar(),
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
_createTopDisplayGestures(),
|
||||
if(_focused() && isWin11)
|
||||
const WindowBorder()
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
Padding _createTopDisplayGestures() => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: _sectionSize * _sectionButtonCount,
|
||||
right: _headerSize * _headerButtonCount,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: _headerSize,
|
||||
child: _createWindowGestures()
|
||||
)
|
||||
);
|
||||
|
||||
GestureDetector _createWindowGestures({Widget? child}) => GestureDetector(
|
||||
onDoubleTap: () {
|
||||
if(!_shouldMaximize){
|
||||
return;
|
||||
}
|
||||
|
||||
appWindow.maximizeOrRestore();
|
||||
_shouldMaximize = false;
|
||||
},
|
||||
onDoubleTapDown: (details) => _shouldMaximize = true,
|
||||
onHorizontalDragStart: (event) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (event) => appWindow.startDragging(),
|
||||
child: child
|
||||
);
|
||||
|
||||
NavigationView _createNavigationView() => NavigationView(
|
||||
paneBodyBuilder: (body) => _createPage(body),
|
||||
pane: NavigationPane(
|
||||
size: const NavigationPaneSize(
|
||||
topHeight: _headerSize
|
||||
),
|
||||
selected: _selectedIndex,
|
||||
onChanged: (index) {
|
||||
_settingsController.player?.pause();
|
||||
_index.value = index;
|
||||
},
|
||||
displayMode: _settingsController.displayType(),
|
||||
indicator: const EndNavigationIndicator(),
|
||||
items: _createItems(),
|
||||
footerItems: _createFooterItems(),
|
||||
header: _settingsController.displayType() != PaneDisplayMode.open ? null : const SizedBox(height: _defaultPadding),
|
||||
autoSuggestBox: _settingsController.displayType() == PaneDisplayMode.top ? null : TextBox(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
placeholder: 'Search',
|
||||
focusNode: _searchFocusNode
|
||||
),
|
||||
autoSuggestBoxReplacement: _settingsController.displayType() == PaneDisplayMode.top ? null : const Icon(FluentIcons.search),
|
||||
),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: _settingsController.displayType() == PaneDisplayMode.top ? null : (child, animation) => child
|
||||
);
|
||||
|
||||
RenderObjectWidget _createPage(Widget? body) => Padding(
|
||||
padding: _createPagePadding(),
|
||||
child: body
|
||||
);
|
||||
|
||||
EdgeInsets _createPagePadding() {
|
||||
if (_settingsController.displayType() == PaneDisplayMode.top) {
|
||||
return const EdgeInsets.all(_defaultPadding);
|
||||
}
|
||||
|
||||
return const EdgeInsets.only(
|
||||
top: 32,
|
||||
left: _defaultPadding,
|
||||
right: _defaultPadding,
|
||||
bottom: _defaultPadding
|
||||
);
|
||||
}
|
||||
|
||||
int? get _selectedIndex {
|
||||
var searchItems = _searchItems();
|
||||
if (searchItems == null) {
|
||||
return _index();
|
||||
}
|
||||
|
||||
if(_index() >= _allItems.length){
|
||||
return null;
|
||||
}
|
||||
|
||||
var indexOnScreen = searchItems.indexOf(_allItems[_index()]);
|
||||
if (indexOnScreen.isNegative) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return indexOnScreen;
|
||||
}
|
||||
|
||||
List<NavigationPaneItem> get _allItems => [..._createItems(), ..._createFooterItems()];
|
||||
|
||||
List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [
|
||||
if(_settingsController.displayType() != PaneDisplayMode.top)
|
||||
PaneItem(
|
||||
title: const Text("Tutorial"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage()
|
||||
)
|
||||
];
|
||||
|
||||
List<NavigationPaneItem> _createItems() => _searchItems() ?? [
|
||||
PaneItem(
|
||||
title: const Text("Home"),
|
||||
icon: const Icon(FluentIcons.game),
|
||||
body: const LauncherPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Backend"),
|
||||
icon: const Icon(FluentIcons.server_enviroment),
|
||||
body: ServerPage()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Settings"),
|
||||
icon: const Icon(FluentIcons.settings),
|
||||
body: SettingsPage()
|
||||
),
|
||||
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
PaneItem(
|
||||
title: const Text("Tutorial"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage()
|
||||
)
|
||||
];
|
||||
|
||||
bool _calculateSize() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_settingsController.saveWindowSize();
|
||||
var width = window.physicalSize.width;
|
||||
PaneDisplayMode? newType;
|
||||
if (width <= 1000) {
|
||||
newType = PaneDisplayMode.top;
|
||||
} else if (width >= 1500) {
|
||||
newType = PaneDisplayMode.open;
|
||||
} else if (width > 1000) {
|
||||
newType = PaneDisplayMode.compact;
|
||||
}
|
||||
|
||||
if(newType == null || newType == _settingsController.displayType()){
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsController.displayType.value = newType;
|
||||
_searchItems.value = null;
|
||||
_searchController.text = "";
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget _createTitleBar() => Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: _createTitleBarContent(),
|
||||
);
|
||||
|
||||
Widget _createTitleBarContent() {
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top) {
|
||||
return WindowTitleBar(focused: _focused());
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
NavigationView(
|
||||
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()
|
||||
),
|
||||
|
||||
PaneItem(
|
||||
title: const Text("Info"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage()
|
||||
)
|
||||
]
|
||||
),
|
||||
SizedBox(
|
||||
width: _settingsController.displayType() == PaneDisplayMode.open ? _openMenuSize : _headerSize,
|
||||
height: _headerSize
|
||||
),
|
||||
|
||||
_createTitleBar(),
|
||||
|
||||
_createGestureHandler(),
|
||||
|
||||
if(_focused && isWin11)
|
||||
const WindowBorder()
|
||||
Expanded(
|
||||
child: _createWindowGestures(
|
||||
child: Container(
|
||||
height: _headerSize,
|
||||
color: Colors.transparent
|
||||
)
|
||||
)
|
||||
),
|
||||
WindowTitleBar(focused: _focused())
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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 * _sectionButtonCount,
|
||||
right: _headerSize * _headerButtonCount,
|
||||
),
|
||||
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()
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
String get searchValue => _searchController.text;
|
||||
}
|
||||
|
||||
@@ -1,62 +1,50 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:dart_vlc/dart_vlc.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide Card;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class InfoPage extends StatelessWidget {
|
||||
import '../controller/settings_controller.dart';
|
||||
|
||||
class InfoPage extends StatefulWidget {
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
_createVersionInfo(),
|
||||
State<InfoPage> createState() => _InfoPageState();
|
||||
}
|
||||
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
_createAutiesAvatar(),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
const Text("Made by Auties00"),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
_createDiscordButton()
|
||||
],
|
||||
),
|
||||
],
|
||||
class _InfoPageState extends State<InfoPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if(_settingsController.player == null){
|
||||
var player = Player(id: 1);
|
||||
player.open(
|
||||
Media.network("https://cdn.discordapp.com/attachments/1006260074416701450/1038844107986055190/tutorial.mp4")
|
||||
);
|
||||
_settingsController.player = player;
|
||||
}
|
||||
|
||||
_settingsController.player?.play();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Card(
|
||||
child: Video(
|
||||
player: _settingsController.player,
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
scale: 1.0,
|
||||
showControls: true,
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Button _createDiscordButton() {
|
||||
return Button(
|
||||
child: const Text("Open file directory"),
|
||||
onPressed: () => launchUrl(Directory(safeBinariesDirectory).uri));
|
||||
}
|
||||
|
||||
CircleAvatar _createAutiesAvatar() {
|
||||
return const CircleAvatar(
|
||||
radius: 48,
|
||||
backgroundImage: AssetImage("assets/images/auties.png"));
|
||||
}
|
||||
|
||||
Align _createVersionInfo() {
|
||||
return const Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Text("Version 4.4${kDebugMode ? '-DEBUG' : ''}")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -6,13 +5,12 @@ import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/username_box.dart';
|
||||
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
|
||||
|
||||
import '../controller/settings_controller.dart';
|
||||
import '../util/reboot.dart';
|
||||
|
||||
class LauncherPage extends StatefulWidget {
|
||||
@@ -27,7 +25,6 @@ class LauncherPage extends StatefulWidget {
|
||||
class _LauncherPageState extends State<LauncherPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BuildController _buildController = Get.find<BuildController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -43,12 +40,12 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
|
||||
int? get _updateTime {
|
||||
var storage = GetStorage("update");
|
||||
return storage.read("last_update");
|
||||
return storage.read("last_update_v2");
|
||||
}
|
||||
|
||||
set _updateTime(int? updateTime) {
|
||||
var storage = GetStorage("update");
|
||||
storage.write("last_update", updateTime);
|
||||
storage.write("last_update_v2", updateTime);
|
||||
}
|
||||
|
||||
void _onCancelWarning() {
|
||||
@@ -65,51 +62,63 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: FutureBuilder(
|
||||
future: _gameController.updater ?? Future.value(true),
|
||||
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,
|
||||
return FutureBuilder(
|
||||
future: _gameController.updater ?? Future.value(true),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData && !snapshot.hasError) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if(snapshot.hasError)
|
||||
_createUpdateError(snapshot),
|
||||
UsernameBox(),
|
||||
const VersionSelector(),
|
||||
GameTypeSelector(),
|
||||
const LaunchButton()
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
ProgressRing(),
|
||||
SizedBox(height: 16.0),
|
||||
Text("Updating Reboot DLL...")
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(snapshot.hasError)
|
||||
_createUpdateError(snapshot),
|
||||
UsernameBox(),
|
||||
const VersionSelector(),
|
||||
GameTypeSelector(),
|
||||
const LaunchButton()
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
|
||||
return const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("Cannot update dll"),
|
||||
severity: InfoBarSeverity.warning
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ErrorDialog(
|
||||
exception: snapshot.error!,
|
||||
stackTrace: snapshot.stackTrace!,
|
||||
errorMessageBuilder: (exception) => "Cannot update Reboot dll: ${snapshot.error}"
|
||||
)
|
||||
);
|
||||
},
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text("Cannot update dll"),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,27 +13,24 @@ class ServerPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Obx(() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(_serverController.warning.value)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: const Text("The lawin server handles authentication and parties, not game hosting"),
|
||||
severity: InfoBarSeverity.info,
|
||||
onClose: () => _serverController.warning.value = false
|
||||
),
|
||||
return Obx(() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(_serverController.warning.value)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: const Text("The backend server handles authentication and parties, not game hosting"),
|
||||
severity: InfoBarSeverity.info,
|
||||
onClose: () => _serverController.warning.value = false
|
||||
),
|
||||
HostInput(),
|
||||
PortInput(),
|
||||
ServerTypeSelector(),
|
||||
const ServerButton()
|
||||
]
|
||||
)),
|
||||
);
|
||||
),
|
||||
HostInput(),
|
||||
PortInput(),
|
||||
ServerTypeSelector(),
|
||||
const ServerButton()
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,60 +19,57 @@ class SettingsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: "The hostname of the server that hosts the multiplayer matches",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Matchmaking Host",
|
||||
placeholder:
|
||||
"Type the hostname of the server that hosts the multiplayer matches",
|
||||
controller: _settingsController.matchmakingIp,
|
||||
validatorMode: AutovalidateMode.always,
|
||||
validator: checkMatchmaking,
|
||||
enabled: _serverController.type() == ServerType.embedded
|
||||
))
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a server is launched",
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: "The hostname of the server that hosts the multiplayer matches",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Matchmaking Host",
|
||||
placeholder:
|
||||
"Type the hostname of the server that hosts the multiplayer matches",
|
||||
controller: _settingsController.matchmakingIp,
|
||||
validatorMode: AutovalidateMode.always,
|
||||
validator: checkMatchmaking,
|
||||
enabled: _serverController.type() == ServerType.embedded
|
||||
))
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a server is launched",
|
||||
child: FileSelector(
|
||||
label: "Reboot DLL",
|
||||
placeholder: "Type the path to the reboot dll",
|
||||
controller: _settingsController.rebootDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a client is launched",
|
||||
child: FileSelector(
|
||||
label: "Console DLL",
|
||||
placeholder: "Type the path to the console dll",
|
||||
controller: _settingsController.consoleDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected to make the game work",
|
||||
child: FileSelector(
|
||||
label: "Reboot DLL",
|
||||
placeholder: "Type the path to the reboot dll",
|
||||
controller: _settingsController.rebootDll,
|
||||
label: "Cranium DLL",
|
||||
placeholder: "Type the path to the dll used for authentication",
|
||||
controller: _settingsController.authDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected when a client is launched",
|
||||
child: FileSelector(
|
||||
label: "Console DLL",
|
||||
placeholder: "Type the path to the console dll",
|
||||
controller: _settingsController.consoleDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always),
|
||||
),
|
||||
Tooltip(
|
||||
message: "The dll that is injected to make the game work",
|
||||
child: FileSelector(
|
||||
label: "Cranium DLL",
|
||||
placeholder: "Type the path to the cranium dll",
|
||||
controller: _settingsController.craniumDll,
|
||||
windowTitle: "Select a dll",
|
||||
folder: false,
|
||||
extension: "dll",
|
||||
validator: checkDll,
|
||||
validatorMode: AutovalidateMode.always))
|
||||
]),
|
||||
);
|
||||
validatorMode: AutovalidateMode.always))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,51 +5,20 @@ import 'package:html/parser.dart' show parse;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_build.dart';
|
||||
import 'package:reboot_launcher/src/util/time.dart';
|
||||
import 'package:reboot_launcher/src/util/version.dart' as parser;
|
||||
import 'package:version/version.dart';
|
||||
|
||||
import 'os.dart';
|
||||
|
||||
const _userAgent =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36";
|
||||
|
||||
final _cookieRegex = RegExp("cookie=\"(.*?);");
|
||||
final _manifestSourceUrl = Uri.parse(
|
||||
"https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md");
|
||||
final _archiveCookieUrl = Uri.parse("http://allinstaller.xyz/rel");
|
||||
final _archiveSourceUrl = Uri.parse("http://allinstaller.xyz/rel?i=1");
|
||||
|
||||
|
||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
var futures = await Future.wait([_fetchArchives(), _fetchManifests()]);
|
||||
return futures.expand((element) => element)
|
||||
.toList()
|
||||
..sort((first, second) => first.version.compareTo(second.version));
|
||||
}
|
||||
|
||||
Future<List<FortniteBuild>> _fetchArchives() async {
|
||||
var cookieResponse = await http.get(_archiveCookieUrl);
|
||||
var cookie = _cookieRegex.firstMatch(cookieResponse.body)?.group(1)?.trim();
|
||||
var response =
|
||||
await http.get(_archiveSourceUrl, headers: {"Cookie": cookie!});
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
}
|
||||
|
||||
var document = parse(response.body);
|
||||
var results = <FortniteBuild>[];
|
||||
for (var build in document.querySelectorAll("a[href^='https']")) {
|
||||
var version = parser.tryParse(build.text.replaceAll("Build ", ""));
|
||||
if (version == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.add(FortniteBuild(
|
||||
version: version, link: build.attributes["href"]!, hasManifest: false));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<List<FortniteBuild>> _fetchManifests() async {
|
||||
var response = await http.get(_manifestSourceUrl);
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
@@ -68,13 +37,13 @@ Future<List<FortniteBuild>> _fetchManifests() async {
|
||||
var name = children[0].text;
|
||||
var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-"));
|
||||
var version = parser
|
||||
.tryParse(minifiedName.replaceFirst("-CL", ""));
|
||||
.tryParse(minifiedName.replaceFirst("", ""));
|
||||
if (version == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var link = children[2].firstChild!.attributes["href"]!;
|
||||
results.add(FortniteBuild(version: version, link: link, hasManifest: true));
|
||||
results.add(FortniteBuild(version: version, link: link));
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -93,9 +62,12 @@ Future<Process> downloadManifestBuild(
|
||||
}
|
||||
|
||||
Future<void> downloadArchiveBuild(String archiveUrl, String destination,
|
||||
Function(double) onProgress, Function() onRar) async {
|
||||
Function(double, String) onProgress, Function() onDecompress) async {
|
||||
var uuid = Random.secure().nextInt(1000000);
|
||||
var extension = archiveUrl.substring(archiveUrl.lastIndexOf("."));
|
||||
var tempFile = File(
|
||||
"$destination\\.temp\\FortniteBuild${Random.secure().nextInt(1000000)}.rar");
|
||||
"$destination\\.temp\\FortniteBuild$uuid$extension"
|
||||
);
|
||||
await tempFile.parent.create(recursive: true);
|
||||
try {
|
||||
var client = http.Client();
|
||||
@@ -106,27 +78,38 @@ Future<void> downloadArchiveBuild(String archiveUrl, String destination,
|
||||
throw Exception("Erroneous status code: ${response.statusCode}");
|
||||
}
|
||||
|
||||
var startTime = DateTime.now();
|
||||
var lastRemaining = -1;
|
||||
var length = response.contentLength!;
|
||||
var received = 0;
|
||||
var sink = tempFile.openWrite();
|
||||
await response.stream.map((s) {
|
||||
received += s.length;
|
||||
onProgress((received / length) * 100);
|
||||
return s;
|
||||
var lastEta = toETA(0);
|
||||
await response.stream.map((entry) {
|
||||
received += entry.length;
|
||||
var percentage = (received / length) * 100;
|
||||
var elapsed = DateTime.now().difference(startTime).inMilliseconds;
|
||||
var newRemaining = (elapsed * length / received - elapsed).round();
|
||||
if(lastRemaining < 0 || lastRemaining - newRemaining <= -10000 || lastRemaining > newRemaining) {
|
||||
lastEta = toETA(lastRemaining = newRemaining);
|
||||
}
|
||||
|
||||
onProgress(percentage, lastEta);
|
||||
return entry;
|
||||
}).pipe(sink);
|
||||
onRar();
|
||||
onDecompress();
|
||||
|
||||
var output = Directory(destination);
|
||||
await output.create(recursive: true);
|
||||
await loadBinary("winrar.exe", true);
|
||||
var shell = Shell(
|
||||
commandVerbose: false,
|
||||
commentVerbose: false,
|
||||
workingDirectory: internalBinariesDirectory
|
||||
workingDirectory: safeBinariesDirectory
|
||||
);
|
||||
await shell.run("./winrar.exe x ${tempFile.path} *.* \"${output.path}\"");
|
||||
await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\"");
|
||||
} finally {
|
||||
if (await tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
if (await tempFile.parent.exists()) {
|
||||
tempFile.parent.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
lib/src/util/error.dart
Normal file
24
lib/src/util/error.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../dialog/dialog.dart';
|
||||
|
||||
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||
if(exception == null){
|
||||
return;
|
||||
}
|
||||
|
||||
if(appKey.currentContext == null || appKey.currentState?.mounted == false){
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
ErrorDialog(
|
||||
exception: exception,
|
||||
stackTrace: stackTrace,
|
||||
errorMessageBuilder: (exception) => framework ? "An error was thrown by Flutter: $exception" : "An uncaught error was thrown: $exception"
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:win32/win32.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'dart:ffi';
|
||||
|
||||
const int appBarSize = 2;
|
||||
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||
|
||||
@@ -31,6 +35,18 @@ Future<File> loadBinary(String binary, bool safe) async{
|
||||
return safeBinary;
|
||||
}
|
||||
|
||||
Future<bool> runElevated(String executable, String args) async {
|
||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||
shellInput.ref.nShow = SW_SHOWDEFAULT;
|
||||
shellInput.ref.fMask = 0x00000040;
|
||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
||||
var shellResult = ShellExecuteEx(shellInput);
|
||||
return shellResult == 1;
|
||||
}
|
||||
|
||||
File _locateInternalBinary(String binary){
|
||||
return File("$internalBinariesDirectory\\$binary");
|
||||
}
|
||||
@@ -42,4 +58,13 @@ Directory get tempDirectory =>
|
||||
Directory("${Platform.environment["Temp"]}");
|
||||
|
||||
String get safeBinariesDirectory =>
|
||||
"${Platform.environment["UserProfile"]}\\.reboot_launcher";
|
||||
"${Platform.environment["UserProfile"]}\\.reboot_launcher";
|
||||
|
||||
File loadEmbedded(String file) {
|
||||
var safeBinary = File("$safeBinariesDirectory\\backend\\cli\\$file");
|
||||
if(safeBinary.existsSync()){
|
||||
return safeBinary;
|
||||
}
|
||||
|
||||
return File("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\$file");
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/util/os.dart';
|
||||
|
||||
const _rebootUrl =
|
||||
"https://nightly.link/Milxnor/Universal-Walking-Simulator/workflows/msbuild/master/Release.zip";
|
||||
"https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip";
|
||||
|
||||
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
||||
return lastUpdateMs != null ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) : null;
|
||||
@@ -29,14 +29,18 @@ Future<int> downloadRebootDll(int? lastUpdateMs) async {
|
||||
var outputDir = await tempDirectory.createTemp("reboot");
|
||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||
|
||||
var rebootDll = outputDir.listSync()
|
||||
.firstWhere((element) => path.extension(element.path) == ".dll");
|
||||
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) {
|
||||
var rebootDll = File(
|
||||
outputDir.listSync()
|
||||
.firstWhere((element) => path.extension(element.path) == ".dll")
|
||||
.path
|
||||
);
|
||||
|
||||
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await rebootDll.readAsBytes())) {
|
||||
outputDir.delete(recursive: true);
|
||||
return now.millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
await rebootDll.rename(oldRebootDll.path);
|
||||
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
|
||||
outputDir.delete(recursive: true);
|
||||
return now.millisecondsSinceEpoch;
|
||||
}
|
||||
17
lib/src/util/selector.dart
Normal file
17
lib/src/util/selector.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
|
||||
Future<String?> openFolderPicker(String title) async =>
|
||||
await FilePicker.platform.getDirectoryPath(dialogTitle: title);
|
||||
|
||||
Future<String?> openFilePicker(String extension) async {
|
||||
var result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowMultiple: false,
|
||||
allowedExtensions: [extension]
|
||||
);
|
||||
if(result == null || result.files.isEmpty){
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.files.first.path;
|
||||
}
|
||||
@@ -1,29 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:process_run/shell.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/os.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
|
||||
final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Lawin.exe");
|
||||
final serverConfig = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Config\\config.ini");
|
||||
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
|
||||
|
||||
const String _serverUrl =
|
||||
"https://cdn.discordapp.com/attachments/1031262639457828910/1034506676843327549/lawin.zip";
|
||||
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> isLawinPortFree() async {
|
||||
try {
|
||||
var portBat = await loadBinary("port.bat", true);
|
||||
@@ -39,27 +25,41 @@ Future<bool> isLawinPortFree() async {
|
||||
|
||||
Future<void> freeLawinPort() async {
|
||||
var releaseBat = await loadBinary("release.bat", false);
|
||||
await Process.run(releaseBat.path, []);
|
||||
var result = await Process.run(releaseBat.path, []);
|
||||
if(!result.outText.contains("Access is denied")){
|
||||
return;
|
||||
}
|
||||
|
||||
await runElevated(releaseBat.path, "");
|
||||
}
|
||||
|
||||
List<String> createRebootArgs(String username, bool headless) {
|
||||
List<String> createRebootArgs(String username, GameType type) {
|
||||
var args = [
|
||||
"-skippatchcheck",
|
||||
"-epicapp=Fortnite",
|
||||
"-epicenv=Prod",
|
||||
"-epiclocale=en-us",
|
||||
"-epicportal",
|
||||
"-noeac",
|
||||
"-fromfl=be",
|
||||
"-fltoken=7ce411021b27b4343a44fdg8",
|
||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
|
||||
"-AUTH_LOGIN=$username@projectreboot.dev",
|
||||
"-AUTH_PASSWORD=Rebooted",
|
||||
"-AUTH_TYPE=epic"
|
||||
"-skippatchcheck",
|
||||
"-nobe",
|
||||
"-fromfl=eac",
|
||||
"-fltoken=3db3ba5dcbd2e16703f3978d",
|
||||
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ"
|
||||
];
|
||||
|
||||
if(headless){
|
||||
args.addAll(["-nullrhi", "-nosplash", "-nosound"]);
|
||||
if(username.isNotEmpty){
|
||||
args.addAll([
|
||||
"-AUTH_LOGIN=${username.replaceAll(RegExp("[^A-Za-z0-9]"), "")}@projectreboot.dev",
|
||||
"-AUTH_PASSWORD=Rebooted",
|
||||
"-AUTH_TYPE=epic"
|
||||
]);
|
||||
}
|
||||
|
||||
if(type == GameType.headlessServer){
|
||||
args.addAll([
|
||||
"-nullrhi",
|
||||
"-nosplash",
|
||||
"-nosound",
|
||||
]);
|
||||
}
|
||||
|
||||
return args;
|
||||
@@ -118,8 +118,7 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
||||
if (!free) {
|
||||
if(!needsFreePort) {
|
||||
return ServerResult(
|
||||
uri: pingSelf(port),
|
||||
type: ServerResultType.ignoreStart
|
||||
type: ServerResultType.alreadyStarted
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,54 +128,22 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
||||
}
|
||||
}
|
||||
|
||||
if(type == ServerType.embedded && !serverLocation.existsSync()){
|
||||
return ServerResult(
|
||||
type: ServerResultType.serverDownloadRequiredError
|
||||
);
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
uri: ping(host, port),
|
||||
type: ServerResultType.canStart
|
||||
);
|
||||
}
|
||||
|
||||
Future<Process?> startEmbeddedServer() async {
|
||||
await resetServerLog();
|
||||
try {
|
||||
var process = await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path);
|
||||
process.outLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append));
|
||||
process.errLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append));
|
||||
return process;
|
||||
} on ProcessException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<HttpServer> startRemoteServer(Uri uri) async {
|
||||
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
|
||||
}
|
||||
|
||||
Future<void> resetServerLog() async {
|
||||
try {
|
||||
if(await serverLogFile.exists()) {
|
||||
await serverLogFile.delete();
|
||||
}
|
||||
|
||||
await serverLogFile.create();
|
||||
}catch(_){
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
class ServerResult {
|
||||
final Future<Uri?>? uri;
|
||||
final int? pid;
|
||||
final Object? error;
|
||||
final StackTrace? stackTrace;
|
||||
final ServerResultType type;
|
||||
|
||||
ServerResult({this.uri, this.pid, this.error, this.stackTrace, required this.type});
|
||||
ServerResult({this.pid, this.error, this.stackTrace, required this.type});
|
||||
}
|
||||
|
||||
enum ServerResultType {
|
||||
@@ -185,10 +152,8 @@ enum ServerResultType {
|
||||
illegalPortError,
|
||||
cannotPingServer,
|
||||
portTakenError,
|
||||
serverDownloadRequiredError,
|
||||
canStart,
|
||||
ignoreStart,
|
||||
started,
|
||||
alreadyStarted,
|
||||
unknownError,
|
||||
stopped,
|
||||
}
|
||||
47
lib/src/util/time.dart
Normal file
47
lib/src/util/time.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
String toETA(int milliseconds){
|
||||
var duration = Duration(milliseconds: milliseconds);
|
||||
return "${duration.inHours.toString().padLeft(2, "0")}:"
|
||||
"${duration.inMinutes.remainder(60).toString().padLeft(2, "0")}:"
|
||||
"${duration.inSeconds.remainder(60).toString().padLeft(2, "0")}";
|
||||
}
|
||||
|
||||
extension DateTimeIso on DateTime {
|
||||
String toIsoString() {
|
||||
String y = (year >= -9999 && year <= 9999) ? _fourDigits(year) : _sixDigits(year);
|
||||
String m = _twoDigits(month);
|
||||
String d = _twoDigits(day);
|
||||
String h = _twoDigits(hour);
|
||||
String min = _twoDigits(minute);
|
||||
String sec = _twoDigits(second);
|
||||
String ms = _threeDigits(millisecond);
|
||||
return "$y-$m-${d}T$h:$min:$sec.${ms}Z";
|
||||
}
|
||||
|
||||
static String _fourDigits(int n) {
|
||||
int absN = n.abs();
|
||||
String sign = n < 0 ? "-" : "";
|
||||
if (absN >= 1000) return "$n";
|
||||
if (absN >= 100) return "${sign}0$absN";
|
||||
if (absN >= 10) return "${sign}00$absN";
|
||||
return "${sign}000$absN";
|
||||
}
|
||||
|
||||
static String _sixDigits(int n) {
|
||||
assert(n < -9999 || n > 9999);
|
||||
int absN = n.abs();
|
||||
String sign = n < 0 ? "-" : "+";
|
||||
if (absN >= 100000) return "$sign$absN";
|
||||
return "${sign}0$absN";
|
||||
}
|
||||
|
||||
static String _threeDigits(int n) {
|
||||
if (n >= 100) return "$n";
|
||||
if (n >= 10) return "0$n";
|
||||
return "00$n";
|
||||
}
|
||||
|
||||
static String _twoDigits(int n) {
|
||||
if (n >= 10) return "$n";
|
||||
return "0$n";
|
||||
}
|
||||
}
|
||||
@@ -37,9 +37,8 @@ class _BuildSelectorState extends State<BuildSelector> {
|
||||
|
||||
ComboBoxItem<FortniteBuild> _createItem(FortniteBuild element) {
|
||||
return ComboBoxItem<FortniteBuild>(
|
||||
value: element,
|
||||
child: Text(
|
||||
"${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"),
|
||||
value: element,
|
||||
child: Text(element.version.toString())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -9,10 +9,10 @@ import 'package:process_run/shell.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/dialog/game_dialogs.dart';
|
||||
import 'package:reboot_launcher/src/dialog/server_dialogs.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/os.dart';
|
||||
import 'package:reboot_launcher/src/util/injector.dart';
|
||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||
@@ -21,6 +21,7 @@ import 'package:reboot_launcher/src/util/server.dart';
|
||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../controller/settings_controller.dart';
|
||||
import '../../dialog/snackbar.dart';
|
||||
|
||||
@@ -34,6 +35,15 @@ class LaunchButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LaunchButtonState extends State<LaunchButton> {
|
||||
final List<String> _errorStrings = [
|
||||
"port 3551 failed: Connection refused",
|
||||
"Unable to login to Fortnite servers",
|
||||
"HTTP 400 response from ",
|
||||
"Network failure when attempting to check platform restrictions",
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
|
||||
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final ServerController _serverController = Get.find<ServerController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
@@ -70,13 +80,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.started.value = true;
|
||||
if (_gameController.username.text.isEmpty) {
|
||||
showMessage("Missing in-game username");
|
||||
if (_gameController.username.text.isEmpty && _gameController.type() != GameType.client) {
|
||||
showMessage("Missing username");
|
||||
_gameController.started.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.started.value = true;
|
||||
if (_gameController.selectedVersionObs.value == null) {
|
||||
showMessage("No version is selected");
|
||||
_gameController.started.value = false;
|
||||
@@ -87,7 +97,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
var version = _gameController.selectedVersionObs.value!;
|
||||
var gamePath = version.executable?.path;
|
||||
if(gamePath == null){
|
||||
_onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null);
|
||||
_onError("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?", null);
|
||||
_onStop();
|
||||
return;
|
||||
}
|
||||
@@ -97,8 +107,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
Win32Process(_gameController.launcherProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
var result = await _serverController.changeStateInteractive(true);
|
||||
if (version.eacExecutable != null) {
|
||||
_gameController.eacProcess = await Process.start(version.eacExecutable!.path, []);
|
||||
Win32Process(_gameController.eacProcess!.pid).suspend();
|
||||
}
|
||||
|
||||
var result = await _serverController.start(
|
||||
required: true,
|
||||
askPortKill: false,
|
||||
);
|
||||
if(!result){
|
||||
showMessage("Cannot launch the game as the backend didn't start up correctly");
|
||||
_onStop();
|
||||
return;
|
||||
}
|
||||
@@ -107,15 +126,16 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
await _logFile!.delete();
|
||||
}
|
||||
|
||||
|
||||
await patch(version.executable!);
|
||||
await compute(patchMatchmaking, version.executable!);
|
||||
await compute(patchHeadless, version.executable!);
|
||||
|
||||
var headlessHosting = _gameController.type() == GameType.headlessServer;
|
||||
var arguments = createRebootArgs(_gameController.username.text, headlessHosting);
|
||||
var arguments = createRebootArgs(_gameController.username.text, _gameController.type.value);
|
||||
_gameController.gameProcess = await Process.start(gamePath, arguments)
|
||||
..exitCode.then((_) => _onEnd())
|
||||
..outLines.forEach((line) => _onGameOutput(line, version.memoryFix))
|
||||
..errLines.forEach((line) => _onGameOutput(line, version.memoryFix));
|
||||
..outLines.forEach((line) => _onGameOutput(line))
|
||||
..errLines.forEach((line) => _onGameOutput(line));
|
||||
_injectOrShowError(Injectable.cranium);
|
||||
if(headlessHosting){
|
||||
await _showServerLaunchingWarning();
|
||||
}
|
||||
@@ -126,17 +146,6 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> patch(File file) async {
|
||||
switch(_gameController.type()){
|
||||
case GameType.client:
|
||||
return await compute(patchMatchmaking, file);
|
||||
case GameType.server:
|
||||
return false;
|
||||
case GameType.headlessServer:
|
||||
return await compute(patchHeadless, file);
|
||||
}
|
||||
}
|
||||
|
||||
void _onEnd() {
|
||||
if(_fail){
|
||||
return;
|
||||
@@ -147,25 +156,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
void _closeDialogIfOpen(bool success) {
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var route = ModalRoute.of(context);
|
||||
var route = ModalRoute.of(appKey.currentContext!);
|
||||
if(route == null || route.isCurrent){
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(success);
|
||||
Navigator.of(appKey.currentContext!).pop(success);
|
||||
}
|
||||
|
||||
Future<void> _showServerLaunchingWarning() async {
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await showDialog<bool>(
|
||||
context: context,
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) => ProgressDialog(
|
||||
text: "Launching headless server...",
|
||||
onStop: () {
|
||||
@@ -182,11 +183,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
_onStop();
|
||||
}
|
||||
|
||||
void _onGameOutput(String line, bool memoryFix) {
|
||||
if(kDebugMode){
|
||||
print(line);
|
||||
}
|
||||
|
||||
void _onGameOutput(String line) {
|
||||
if(_logFile != null){
|
||||
_logFile!.writeAsString("$line\n", mode: FileMode.append);
|
||||
}
|
||||
@@ -196,33 +193,18 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("port 3551 failed: Connection refused") || line.contains("Unable to login to Fortnite servers")){
|
||||
if(_errorStrings.any((element) => line.contains(element))){
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showBrokenError();
|
||||
_showTokenError();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("HTTP 400 response from ")){
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showUnsupportedHeadless();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showTokenError();
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Platform has ")){
|
||||
_injectOrShowError(Injectable.cranium);
|
||||
return;
|
||||
}
|
||||
|
||||
if(line.contains("Login: Completing Sign-in")){
|
||||
if(line.contains("Region ")){
|
||||
if(_gameController.type.value == GameType.client){
|
||||
_injectOrShowError(Injectable.console);
|
||||
}else {
|
||||
@@ -230,9 +212,19 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
.then((value) => _closeDialogIfOpen(true));
|
||||
}
|
||||
|
||||
if(memoryFix){
|
||||
_injectOrShowError(Injectable.memoryFix);
|
||||
}
|
||||
_injectOrShowError(Injectable.memoryFix);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showTokenError() async {
|
||||
if(_serverController.type() == ServerType.embedded) {
|
||||
showTokenErrorFixable();
|
||||
await _serverController.start(
|
||||
required: true,
|
||||
askPortKill: false
|
||||
);
|
||||
} else {
|
||||
showTokenErrorUnfixable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +263,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
await injectDll(gameProcess.pid, dllPath.path);
|
||||
} catch (exception) {
|
||||
showSnackbar(
|
||||
context,
|
||||
appKey.currentContext!,
|
||||
Snackbar(
|
||||
content: Text("Cannot inject $injectable.dll: $exception", textAlign: TextAlign.center),
|
||||
extended: true
|
||||
@@ -283,6 +275,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
|
||||
void _onDllFail(File dllPath) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if(_fail){
|
||||
return;
|
||||
}
|
||||
|
||||
_fail = true;
|
||||
_closeDialogIfOpen(false);
|
||||
showMissingDllError(path.basename(dllPath.path));
|
||||
@@ -297,9 +293,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
case Injectable.console:
|
||||
return File(_settingsController.consoleDll.text);
|
||||
case Injectable.cranium:
|
||||
return File(_settingsController.craniumDll.text);
|
||||
return File(_settingsController.authDll.text);
|
||||
case Injectable.memoryFix:
|
||||
return await loadBinary("fix.dll", true);
|
||||
return await loadBinary("leakv2.dll", true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||
import 'package:reboot_launcher/src/dialog/add_local_version.dart';
|
||||
import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart';
|
||||
@@ -12,6 +14,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../dialog/add_server_version.dart';
|
||||
import '../../util/checks.dart';
|
||||
import '../shared/file_selector.dart';
|
||||
|
||||
class VersionSelector extends StatefulWidget {
|
||||
const VersionSelector({Key? key}) : super(key: key);
|
||||
@@ -58,9 +61,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
Widget _createSelector(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The version of Fortnite to launch",
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Obx(() => DropDownButton(
|
||||
child: Obx(() => DropDownButton(
|
||||
leading: Text(_gameController.selectedVersionObs.value?.name ??
|
||||
"Select a version"),
|
||||
items: _gameController.hasNoVersions
|
||||
@@ -68,28 +69,26 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
: _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList()))
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(
|
||||
BuildContext context, FortniteVersion version) {
|
||||
return MenuFlyoutItem(
|
||||
text: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse ||
|
||||
event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
text: Listener(
|
||||
onPointerDown: (event) async {
|
||||
if (event.kind != PointerDeviceKind.mouse ||
|
||||
event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _openMenu(context, version, event.position);
|
||||
},
|
||||
child: Text(version.name)
|
||||
)
|
||||
await _openMenu(context, version, event.position);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(version.name)
|
||||
),
|
||||
),
|
||||
trailing: const Expanded(child: SizedBox()),
|
||||
onPressed: () => _gameController.selectedVersion = version);
|
||||
}
|
||||
|
||||
@@ -120,7 +119,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
context: context,
|
||||
offset: offset,
|
||||
builder: (context) => MenuFlyout(
|
||||
items: ContextualOption.getValues(version.memoryFix)
|
||||
items: ContextualOption.values
|
||||
.map((entry) => _createOption(context, entry))
|
||||
.toList()
|
||||
)
|
||||
@@ -137,18 +136,13 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
.onError((error, stackTrace) => _onExplorerError());
|
||||
break;
|
||||
|
||||
case ContextualOption.rename:
|
||||
case ContextualOption.modify:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop();
|
||||
var result = await _openRenameDialog(context, version);
|
||||
if(result == null){
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.rename(version, result);
|
||||
await _openRenameDialog(context, version);
|
||||
break;
|
||||
|
||||
case ContextualOption.delete:
|
||||
@@ -173,24 +167,6 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
break;
|
||||
case ContextualOption.enableMemoryFix:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
version.memoryFix = true;
|
||||
Navigator.of(context).pop();
|
||||
showMessage("Enabled memory fix");
|
||||
break;
|
||||
case ContextualOption.disableMemoryFix:
|
||||
if(!mounted){
|
||||
return;
|
||||
}
|
||||
|
||||
version.memoryFix = false;
|
||||
Navigator.of(context).pop();
|
||||
showMessage("Disabled memory fix");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
@@ -251,46 +227,57 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
}
|
||||
|
||||
Future<String?> _openRenameDialog(BuildContext context, FortniteVersion version) {
|
||||
var controller = TextEditingController(text: version.name);
|
||||
var nameController = TextEditingController(text: version.name);
|
||||
var pathController = TextEditingController(text: version.location.path);
|
||||
return showDialog<String?>(
|
||||
context: context,
|
||||
builder: (context) => Form(
|
||||
child: Builder(
|
||||
builder: (context) => ContentDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormBox(
|
||||
controller: controller,
|
||||
header: "Name",
|
||||
placeholder: "Type the new version name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||
),
|
||||
builder: (context) => FormDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormBox(
|
||||
controller: nameController,
|
||||
header: "Name",
|
||||
placeholder: "Type the new version name",
|
||||
autofocus: true,
|
||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (!Form.of(context)!.validate()) {
|
||||
return;
|
||||
}
|
||||
const SizedBox(
|
||||
height: 16.0
|
||||
),
|
||||
|
||||
Navigator.of(context).pop(controller.text);
|
||||
},
|
||||
child: const Text('Save')
|
||||
)
|
||||
]
|
||||
FileSelector(
|
||||
label: "Location",
|
||||
placeholder: "Type the new game folder",
|
||||
windowTitle: "Select game folder",
|
||||
controller: pathController,
|
||||
validator: checkGameFolder,
|
||||
folder: true
|
||||
),
|
||||
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
buttons: [
|
||||
DialogButton(
|
||||
type: ButtonType.secondary
|
||||
),
|
||||
|
||||
DialogButton(
|
||||
text: "Save",
|
||||
type: ButtonType.primary,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_gameController.updateVersion(version, (version) {
|
||||
version.name = nameController.text;
|
||||
version.location = Directory(pathController.text);
|
||||
});
|
||||
},
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -298,22 +285,12 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
|
||||
enum ContextualOption {
|
||||
openExplorer,
|
||||
rename,
|
||||
enableMemoryFix,
|
||||
disableMemoryFix,
|
||||
modify,
|
||||
delete;
|
||||
|
||||
static List<ContextualOption> getValues(bool memoryFix){
|
||||
return memoryFix
|
||||
? [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.disableMemoryFix, ContextualOption.delete]
|
||||
: [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.enableMemoryFix, ContextualOption.delete];
|
||||
}
|
||||
|
||||
String get name {
|
||||
return this == ContextualOption.openExplorer ? "Open in explorer"
|
||||
: this == ContextualOption.rename ? "Rename"
|
||||
: this == ContextualOption.enableMemoryFix ? "Enable memory leak fix"
|
||||
: this == ContextualOption.disableMemoryFix ? "Disable memory leak fix"
|
||||
: this == ContextualOption.modify ? "Modify"
|
||||
: "Delete";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ class HostInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The hostname of the lawin server",
|
||||
message: "The hostname of the backend server",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Host",
|
||||
placeholder: "Type the lawin server's hostname",
|
||||
placeholder: "Type the backend server's hostname",
|
||||
controller: _serverController.host,
|
||||
enabled: _serverController.type.value == ServerType.remote
|
||||
))
|
||||
|
||||
@@ -13,10 +13,10 @@ class PortInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The port of the lawin server",
|
||||
message: "The port of the backend server",
|
||||
child: Obx(() => SmartInput(
|
||||
label: "Port",
|
||||
placeholder: "Type the lawin server's port",
|
||||
placeholder: "Type the backend server's port",
|
||||
controller: _serverController.port,
|
||||
enabled: _serverController.type.value != ServerType.embedded
|
||||
))
|
||||
|
||||
@@ -23,7 +23,10 @@ class _ServerButtonState extends State<ServerButton> {
|
||||
child: Obx(() => Tooltip(
|
||||
message: _helpMessage,
|
||||
child: Button(
|
||||
onPressed: () async => _serverController.changeStateInteractive(false),
|
||||
onPressed: () async => _serverController.start(
|
||||
required: false,
|
||||
askPortKill: true
|
||||
),
|
||||
child: Text(_buttonText())),
|
||||
)),
|
||||
),
|
||||
@@ -46,18 +49,18 @@ class _ServerButtonState extends State<ServerButton> {
|
||||
switch(_serverController.type.value){
|
||||
case ServerType.embedded:
|
||||
if (_serverController.started.value) {
|
||||
return "Stop the lawin server currently running";
|
||||
return "Stop the backend server currently running";
|
||||
}
|
||||
|
||||
return "Start a new local lawin server";
|
||||
return "Start a new local backend 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";
|
||||
return "Start a reverse proxy targeting the remote backend server";
|
||||
case ServerType.local:
|
||||
return "Check if a local lawin server is running";
|
||||
return "Check if a local backend server is running";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class ServerTypeSelector extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "Determines the type of lawin server to use",
|
||||
message: "Determines the type of backend server to use",
|
||||
child: InfoLabel(
|
||||
label: "Type",
|
||||
child: SizedBox(
|
||||
@@ -35,7 +35,10 @@ class ServerTypeSelector extends StatelessWidget {
|
||||
child: Text(type.name)
|
||||
)
|
||||
),
|
||||
onPressed: () => _serverController.type(type)
|
||||
onPressed: () async {
|
||||
await _serverController.stop();
|
||||
_serverController.type(type);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
106
lib/src/widget/shared/file_selector.dart
Normal file
106
lib/src/widget/shared/file_selector.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||
|
||||
import '../../util/selector.dart';
|
||||
|
||||
class FileSelector extends StatefulWidget {
|
||||
final String label;
|
||||
final String placeholder;
|
||||
final String windowTitle;
|
||||
final bool allowNavigator;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?) validator;
|
||||
final AutovalidateMode? validatorMode;
|
||||
final String? extension;
|
||||
final bool folder;
|
||||
|
||||
const FileSelector(
|
||||
{required this.label,
|
||||
required this.placeholder,
|
||||
required this.windowTitle,
|
||||
required this.controller,
|
||||
required this.validator,
|
||||
required this.folder,
|
||||
this.extension,
|
||||
this.validatorMode,
|
||||
this.allowNavigator = true,
|
||||
Key? key})
|
||||
: assert(folder || extension != null, "Missing extension for file selector"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<FileSelector> createState() => _FileSelectorState();
|
||||
}
|
||||
|
||||
class _FileSelectorState extends State<FileSelector> {
|
||||
final RxBool _valid = RxBool(true);
|
||||
late String? Function(String?) validator;
|
||||
bool _selecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
validator = (value) {
|
||||
var result = widget.validator(value);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _valid.value = result == null);
|
||||
return result;
|
||||
};
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfoLabel(
|
||||
label: widget.label,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormBox(
|
||||
controller: widget.controller,
|
||||
placeholder: widget.placeholder,
|
||||
validator: validator,
|
||||
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
|
||||
)
|
||||
),
|
||||
if (widget.allowNavigator) const SizedBox(width: 8.0),
|
||||
if (widget.allowNavigator)
|
||||
Tooltip(
|
||||
message: "Select a ${widget.folder ? 'folder' : 'file'}",
|
||||
child: Obx(() => Padding(
|
||||
padding: _valid() ? EdgeInsets.zero : const EdgeInsets.only(bottom: 21.0),
|
||||
child: Button(
|
||||
onPressed: _onPressed,
|
||||
child: const Icon(FluentIcons.open_folder_horizontal)
|
||||
))
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void _onPressed() {
|
||||
if(_selecting){
|
||||
showMessage("Folder selector is already opened");
|
||||
return;
|
||||
}
|
||||
|
||||
_selecting = true;
|
||||
if(widget.folder) {
|
||||
compute(openFolderPicker, widget.windowTitle)
|
||||
.then((value) => widget.controller.text = value ?? widget.controller.text)
|
||||
.then((_) => _selecting = false);
|
||||
return;
|
||||
}
|
||||
|
||||
compute(openFilePicker, widget.extension!)
|
||||
.then((value) => widget.controller.text = value ?? widget.controller.text)
|
||||
.then((_) => _selecting = false);
|
||||
}
|
||||
}
|
||||
190
lib/src/widget/shared/fluent_card.dart
Normal file
190
lib/src/widget/shared/fluent_card.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class FluentCard extends StatefulWidget {
|
||||
const FluentCard({
|
||||
Key? key,
|
||||
this.leading,
|
||||
required this.content,
|
||||
this.icon,
|
||||
this.trailing,
|
||||
this.animationCurve,
|
||||
this.animationDuration,
|
||||
this.onPressed,
|
||||
this.onStateChanged,
|
||||
this.isButton = false,
|
||||
this.headerHeight = 68.5,
|
||||
this.headerBackgroundColor,
|
||||
this.contentBackgroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
static Color backgroundColor(ThemeData style, Set<ButtonStates> states, [bool isClickable = true]) {
|
||||
if (style.brightness == Brightness.light) {
|
||||
if (!states.isDisabled && isClickable) {
|
||||
if (states.isPressing) return const ColorConst.withOpacity(0xf9f9f9, 0.2);
|
||||
if (states.isHovering) return const ColorConst.withOpacity(0xf9f9f9, 0.4);
|
||||
}
|
||||
return const ColorConst.withOpacity(0xFFFFFF, 0.7);
|
||||
} else {
|
||||
if (!states.isDisabled && isClickable) {
|
||||
if (states.isPressing) return const ColorConst.withOpacity(0xFFFFFF, 0.03);
|
||||
if (states.isHovering) return const ColorConst.withOpacity(0xFFFFFF, 0.082);
|
||||
}
|
||||
return const ColorConst.withOpacity(0xFFFFFF, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
static Color borderColor(ThemeData style, Set<ButtonStates> states, [bool isClickable = true]) {
|
||||
if (style.brightness == Brightness.light) {
|
||||
if (isClickable && states.isHovering && !states.isPressing) return const Color(0xFF212121).withOpacity(0.22);
|
||||
return const Color(0xFF212121).withOpacity(0.17);
|
||||
} else {
|
||||
if (isClickable && states.isPressing) return Colors.white.withOpacity(0.062);
|
||||
if (isClickable && states.isHovering) return Colors.white.withOpacity(0.02);
|
||||
return Colors.black.withOpacity(0.52);
|
||||
}
|
||||
}
|
||||
|
||||
/// The leading widget.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Icon]
|
||||
/// * [RadioButton]
|
||||
/// * [Checkbox]
|
||||
final Widget? leading;
|
||||
|
||||
/// The card content
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget content;
|
||||
|
||||
/// The icon of the toggle button.
|
||||
final Widget? icon;
|
||||
|
||||
/// Disable when onPressed is null, always show chevron icon in the right
|
||||
final bool isButton;
|
||||
|
||||
/// The trailing widget. It's positioned at the right of [content]
|
||||
/// and at the left of [icon].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ToggleSwitch]
|
||||
final Widget? trailing;
|
||||
|
||||
/// Makes the card clickable
|
||||
/// is null by default
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The expand-collapse animation duration. If null, defaults to
|
||||
/// [FluentTheme.fastAnimationDuration]
|
||||
final Duration? animationDuration;
|
||||
|
||||
/// The expand-collapse animation curve. If null, defaults to
|
||||
/// [FluentTheme.animationCurve]
|
||||
final Curve? animationCurve;
|
||||
|
||||
/// A callback called when the current state is changed. `true` when
|
||||
/// open and `false` when closed.
|
||||
final ValueChanged<bool>? onStateChanged;
|
||||
|
||||
/// The height of the header.
|
||||
///
|
||||
/// Defaults to 48.0
|
||||
final double headerHeight;
|
||||
|
||||
/// The background color of the header. If null, [ThemeData.scaffoldBackgroundColor]
|
||||
/// is used
|
||||
final Color? headerBackgroundColor;
|
||||
|
||||
/// The content color of the header. If null, [ThemeData.acrylicBackgroundColor]
|
||||
/// is used
|
||||
final Color? contentBackgroundColor;
|
||||
|
||||
@override
|
||||
FluentCardState createState() => FluentCardState();
|
||||
}
|
||||
|
||||
class FluentCardState extends State<FluentCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late ThemeData theme;
|
||||
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.animationDuration ?? const Duration(milliseconds: 150),
|
||||
);
|
||||
}
|
||||
|
||||
static void emptyPressMethod() {}
|
||||
static const double borderSize = 0.5;
|
||||
static final Color darkBorderColor = Colors.black.withOpacity(0.8);
|
||||
|
||||
static const Duration expanderAnimationDuration = Duration(milliseconds: 70);
|
||||
|
||||
/// If this widget acts as a button and is disabled, gray out all text and icons
|
||||
Widget buttonStyled(Widget child) => !widget.isButton || widget.onPressed != null ? child : IconTheme.merge(
|
||||
data: IconThemeData(color: theme.disabledColor),
|
||||
child: DefaultTextStyle.merge(style: TextStyle(color: theme.disabledColor), child: child)
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final isLtr = Directionality.of(context) == TextDirection.ltr;
|
||||
theme = FluentTheme.of(context);
|
||||
bool isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return buttonStyled(HoverButton(
|
||||
onPressed: widget.onPressed ?? (widget.isButton ? null : emptyPressMethod),
|
||||
builder: (context, states) {
|
||||
return AnimatedContainer(
|
||||
duration: expanderAnimationDuration,
|
||||
height: widget.headerHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: FluentCard.backgroundColor(theme, states, widget.onPressed != null),
|
||||
border: Border.all(
|
||||
width: borderSize,
|
||||
color: FluentCard.borderColor(theme, states, widget.onPressed != null),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.only(start: 16.0),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (widget.leading != null) Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 17.0),
|
||||
child: widget.leading!,
|
||||
),
|
||||
Expanded(child: widget.content),
|
||||
if (widget.trailing != null) Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 20.0, end: 13.5),
|
||||
child: widget.trailing!,
|
||||
),
|
||||
if (widget.icon != null || widget.isButton) Container(
|
||||
margin: EdgeInsetsDirectional.only(
|
||||
start: widget.trailing != null ? 8.0 : 20.0,
|
||||
end: 8.0,
|
||||
top: 8.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
alignment: Alignment.center,
|
||||
child: widget.icon ?? Icon(isLtr ? isDark ? FluentIcons.chevron_right : FluentIcons.chevron_right_med :
|
||||
isDark ? FluentIcons.chevron_left : FluentIcons.chevron_left_med, size: 11),
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class ColorConst extends Color {
|
||||
const ColorConst.withOpacity(int value, double opacity) : super(
|
||||
( (((opacity * 0xff ~/ 1) & 0xff) << 24) | ((0x00ffffff & value)) ) & 0xFFFFFFFF);
|
||||
}
|
||||
Reference in New Issue
Block a user