Reboot v3

This commit is contained in:
Alessandro Autiero
2023-05-24 23:19:36 +02:00
parent 760e336bc0
commit eb6381912c
129 changed files with 396379 additions and 1380 deletions

View File

@@ -7,14 +7,12 @@ import 'package:reboot_launcher/src/cli/game.dart';
import 'package:reboot_launcher/src/cli/reboot.dart';
import 'package:reboot_launcher/src/cli/server.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server.dart' as server;
late String? username;
late GameType type;
late bool host;
late bool verbose;
late String dll;
late FortniteVersion version;
@@ -40,10 +38,10 @@ void main(List<String> args) async {
..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))
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? rebootDllFile)
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
..addFlag("log", defaultsTo: false)
..addFlag("host", defaultsTo: false)
..addFlag("auto-restart", defaultsTo: false, negatable: true);
var result = parser.parse(args);
if (result.command?.name == "list") {
@@ -54,8 +52,8 @@ void main(List<String> args) async {
}
dll = result["dll"];
type = getGameType(result);
username = result["username"] ?? gameJson["${type == GameType.client ? "game" : "server"}_username"];
host = result["host"];
username = result["username"] ?? gameJson["username"];
verbose = result["log"];
version = _createVersion(gameJson["version"], result["version"], versions);
@@ -69,7 +67,7 @@ void main(List<String> args) async {
}
}
stdout.writeln("Launching game(type: ${type.name})...");
stdout.writeln("Launching game...");
if(version.executable == null){
throw Exception("Missing game executable at: ${version.location.path}");
}
@@ -78,9 +76,9 @@ void main(List<String> args) async {
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 serverHost = result["server-host"] ?? serverJson["${serverType.id}_host"];
var serverPort = result["server-port"] ?? serverJson["${serverType.id}_port"];
var started = await startServer(serverHost, serverPort, serverType);
if(!started){
stderr.writeln("Cannot start server!");
return;

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
@@ -7,32 +6,36 @@ import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/cli.dart';
import 'package:reboot_launcher/src/controller/build_controller.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/page/home_page.dart';
import 'package:reboot_launcher/src/util/error.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/page/home_page.dart';
import 'package:system_theme/system_theme.dart';
import 'package:window_manager/window_manager.dart';
const double kDefaultWindowWidth = 885;
const double kDefaultWindowHeight = 885;
final GlobalKey appKey = GlobalKey();
void main() async {
await safeBinariesDirectory.create(recursive: true);
await installationDirectory.create(recursive: true);
WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load();
await GetStorage.init("game");
await GetStorage.init("server");
await GetStorage.init("update");
await GetStorage.init("settings");
await GetStorage.init("reboot_game");
await GetStorage.init("reboot_server");
await GetStorage.init("reboot_update");
await GetStorage.init("reboot_settings");
await GetStorage.init("reboot_hosting");
Get.put(GameController());
Get.put(ServerController());
Get.put(BuildController());
Get.put(SettingsController());
Get.put(HostingController());
doWhenWindowReady(() {
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight);
var controller = Get.find<SettingsController>();
var size = Size(controller.width, controller.height);
var window = appWindow as WinDesktopWindow;
@@ -44,14 +47,16 @@ void main() async {
appWindow.alignment = Alignment.center;
}
windowManager.setPreventClose(true);
appWindow.title = "Reboot Launcher";
appWindow.show();
});
runZonedGuarded(() =>
runApp(const RebootApplication()),
(error, stack) => onError(error, stack, false)
runZonedGuarded(
() async => runApp(const RebootApplication()),
(error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
)
);
}
@@ -64,27 +69,22 @@ class RebootApplication extends StatefulWidget {
class _RebootApplicationState extends State<RebootApplication> {
@override
Widget build(BuildContext context) {
final color = SystemTheme.accentColor.accent.toAccentColor();
return FluentApp(
title: "Reboot Launcher",
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
color: color,
darkTheme: _createTheme(Brightness.dark),
theme: _createTheme(Brightness.light),
home: HomePage(key: appKey),
);
}
Widget build(BuildContext context) => FluentApp(
title: "Reboot Launcher",
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
color: SystemTheme.accentColor.accent.toAccentColor(),
darkTheme: _createTheme(Brightness.dark),
theme: _createTheme(Brightness.light),
home: HomePage(key: appKey),
);
FluentThemeData _createTheme(Brightness brightness) {
return FluentThemeData(
brightness: brightness,
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
visualDensity: VisualDensity.standard,
focusTheme: FocusThemeData(
glowFactor: is10footScreen() ? 2.0 : 0.0,
),
);
}
FluentThemeData _createTheme(Brightness brightness) => FluentThemeData(
brightness: brightness,
accentColor: SystemTheme.accentColor.accent.toAccentColor(),
visualDensity: VisualDensity.standard,
focusTheme: FocusThemeData(
glowFactor: is10footScreen() ? 2.0 : 0.0,
),
);
}

View File

@@ -3,11 +3,8 @@ 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) {
@@ -15,15 +12,6 @@ String getDefaultServerType(Map<String, dynamic> json) {
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){
@@ -33,18 +21,6 @@ ServerType getServerType(ArgResults result) {
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))

View File

@@ -4,7 +4,6 @@ import 'package:process_run/shell.dart';
import 'package:reboot_launcher/cli.dart';
import '../model/fortnite_version.dart';
import '../model/game_type.dart';
import '../util/injector.dart';
import '../util/os.dart';
import '../util/process.dart';
@@ -32,15 +31,14 @@ Future<void> startGame() async {
.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'}";
username = "Reboot${host ? '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, ""))
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, host, ""))
..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose));
}
@@ -68,7 +66,7 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
}
if(line.contains("Platform has ")){
_injectOrShowError("craniumv2.dll");
_injectOrShowError("cobalt.dll");
return;
}
@@ -90,7 +88,7 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
_injectOrShowError("console.dll");
}
_injectOrShowError("leakv2.dll");
_injectOrShowError("memoryleak.dll");
}
}
@@ -107,7 +105,7 @@ Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
try {
stdout.writeln("Injecting $binary...");
var dll = locate ? await loadBinary(binary, true) : File(binary);
var dll = locate ? File("${assetsDirectory.path}\\dlls\\$binary") : File(binary);
if(!dll.existsSync()){
throw Exception("Cannot inject $dll: missing file");
}

View File

@@ -1,18 +1,19 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:reboot_launcher/src/util/server.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 _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memoryleak.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);
var consoleDll = File("${assetsDirectory.path}\\dlls\\console.dll");
if(!consoleDll.existsSync()){
var response = await http.get(Uri.parse(_consoleDownload));
if(response.statusCode != 200){
@@ -22,30 +23,27 @@ Future<void> downloadRequiredDLLs() async {
await consoleDll.writeAsBytes(response.bodyBytes);
}
var craniumDll = await loadBinary("craniumv2.dll", true);
var craniumDll = File("${assetsDirectory.path}\\dlls\\cobalt.dll");
if(!craniumDll.existsSync()){
var response = await http.get(Uri.parse(_baseDownload));
if(response.statusCode != 200){
throw Exception("Cannot download craniumv2.dll");
throw Exception("Cannot download cobalt.dll");
}
await craniumDll.writeAsBytes(response.bodyBytes);
}
var memoryFixDll = await loadBinary("leakv2.dll", true);
var memoryFixDll = File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
if(!memoryFixDll.existsSync()){
var response = await http.get(Uri.parse(_memoryFixDownload));
if(response.statusCode != 200){
throw Exception("Cannot download leakv2.dll");
throw Exception("Cannot download memoryleak.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()){
if(!serverDirectory.existsSync()){
var response = await http.get(Uri.parse(_embeddedConfigDownload));
if(response.statusCode != 200){
throw Exception("Cannot download embedded server config");
@@ -53,7 +51,6 @@ Future<void> downloadRequiredDLLs() async {
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, "${safeBinariesDirectory.path}\\cli");
await extractFileToDisk(tempZip.path, serverDirectory.path);
}
}

View File

@@ -20,7 +20,7 @@ Future<bool> startServer(String? host, String? port, ServerType type) async {
return true;
case ServerType.embedded:
stdout.writeln("Starting an embedded server...");
await server.startServer();
await server.startServer(false);
var result = await server.ping(host ?? "127.0.0.1", port ?? "3551");
if(result == null){
throw Exception("Cannot start embedded server");

View File

@@ -1,7 +1,5 @@
import 'dart:io';
import 'game_type.dart';
class GameInstance {
final Process gameProcess;
final Process? launcherProcess;

View File

@@ -1,26 +0,0 @@
enum GameType {
client,
server,
headlessServer;
static GameType? of(String id){
try {
return GameType.values
.firstWhere((element) => element.id == id);
}catch(_){
return null;
}
}
String get id {
return this == GameType.client ? "client"
: this == GameType.server ? "server"
: "headless_server";
}
String get name {
return this == GameType.client ? "Game client"
: this == GameType.server ? "Game server"
: "Headless game server";
}
}

View File

@@ -1,11 +0,0 @@
import 'package:version/version.dart';
class RebootDownload {
final int updateTime;
final Object? error;
final StackTrace? stackTrace;
RebootDownload(this.updateTime, [this.error, this.stackTrace]);
bool get hasError => error != null;
}

View File

@@ -0,0 +1,8 @@
enum UpdateStatus {
waiting,
started,
success,
error;
bool isDone() => this == UpdateStatus.success || this == UpdateStatus.error;
}

View File

@@ -1,280 +0,0 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/model/reboot_download.dart';
import 'package:reboot_launcher/src/util/os.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/version_selector.dart';
import 'package:reboot_launcher/src/widget/shared/setting_tile.dart';
import '../dialog/dialog_button.dart';
import '../util/checks.dart';
import '../util/reboot.dart';
class LauncherPage extends StatefulWidget {
const LauncherPage(
{Key? key})
: super(key: key);
@override
State<LauncherPage> createState() => _LauncherPageState();
}
class _LauncherPageState extends State<LauncherPage> {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final BuildController _buildController = Get.find<BuildController>();
@override
void initState() {
if(_gameController.updater == null) {
_startUpdater();
_setupBuildWarning();
}
super.initState();
}
void _setupBuildWarning() {
_buildController.cancelledDownload
.listen((value) => value ? _onCancelWarning() : {});
}
void _startUpdater() {
_gameController.updater = StreamController.broadcast();
downloadRebootDll(_settingsController.updateUrl.text, _updateTime)
..then((result) async {
if(!result.hasError){
_updateTime = result.updateTime;
_gameController.updated = true;
_gameController.failing = false;
_gameController.error = false;
_gameController.updater?.add(true);
return;
}
if(_gameController.failing){
_gameController.updated = false;
_gameController.failing = false;
_gameController.error = true;
_gameController.updater?.add(false);
return;
}
_gameController.failing = true;
showDialog(
context: appKey.currentContext!,
builder: (context) => InfoDialog(
text: "An error occurred while downloading the reboot dll: this usually means that your antivirus flagged it. "
"Do you want to add an exclusion to Windows Defender to fix the issue? "
"If you are using a different antivirus disable it manually as this won't work. ",
buttons: [
ErrorDialog.createCopyErrorButton(
error: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace,
type: ButtonType.secondary,
onClick: () {
Navigator.pop(context);
_gameController.updated = false;
_gameController.failing = false;
_gameController.error = true;
_gameController.updater?.add(false);
}
),
DialogButton(
text: "Add",
type: ButtonType.primary,
onTap: () async {
Navigator.pop(context);
var binary = await loadBinary("antivirus.bat", true);
var result = await runElevated(binary.path, "");
if(!result) {
_gameController.failing = false;
}
_startUpdater();
}
),
],
)
);
})
..catchError((error, stackTrace) {
_gameController.error = true;
_gameController.updater?.add(false);
return RebootDownload(0, error, stackTrace);
});
}
int? get _updateTime {
var storage = GetStorage("update");
return storage.read("last_update_v2");
}
set _updateTime(int? updateTime) {
var storage = GetStorage("update");
storage.write("last_update_v2", updateTime);
}
void _onCancelWarning() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(!mounted) {
return;
}
showSnackbar(context,
const Snackbar(content: Text("Download cancelled")));
_buildController.cancelledDownload(false);
});
}
@override
Widget build(BuildContext context) => StreamBuilder<bool>(
stream: _gameController.updater!.stream,
builder: (context, snapshot) => !_gameController.updated && !_gameController.error ? _updateScreen : _homeScreen
);
Widget get _homeScreen => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _gameController.error ? _updateError : const SizedBox(),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: SizedBox(height: _gameController.error ? 16.0 : 0.0),
),
SettingTile(
title: "Username",
subtitle: "Enter the name that others will see once you are in-game",
content: TextFormBox(
placeholder: "username",
controller: _gameController.username,
validator: checkMatchmaking,
autovalidateMode: AutovalidateMode.always
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Matchmaking host",
subtitle: "Enter the IP address of the game server hosting the match",
content: TextFormBox(
placeholder: "ip:port",
controller: _settingsController.matchmakingIp,
validator: checkMatchmaking,
autovalidateMode: AutovalidateMode.always
),
expandedContent: [
ListTile(
title: const Text(
"Automatically start a game server",
style: TextStyle(
fontSize: 14
),
),
subtitle: const Text("Choose whether an headless server should be automatically started when matchmaking is on localhost"),
trailing: Obx(() => ToggleSwitch(
checked: _gameController.autostartGameServer(),
onChanged: (value) => _gameController.autostartGameServer.value = value
))
),
],
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Version",
subtitle: "Select the version of Fortnite you want to play with your friends",
content: const VersionSelector(),
expandedContent: [
ListTile(
title: const Text(
"Add a version from this PC's local storage",
style: TextStyle(
fontSize: 14
),
),
trailing: Button(
onPressed: () => VersionSelector.openAddDialog(context),
child: const Text("Add build "),
),
),
ListTile(
title: const Text(
"Download any version from the cloud",
style: TextStyle(
fontSize: 14
),
),
trailing: Button(
onPressed: () => VersionSelector.openDownloadDialog(context),
child: const Text("Download"),
),
),
]
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Instance type",
subtitle: "Select the type of instance you want to launch",
content: GameTypeSelector()
),
const Expanded(child: SizedBox()),
const LaunchButton()
],
);
Widget get _updateScreen => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
],
);
Widget get _updateError {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
_gameController.updated = false;
_gameController.failing = false;
_gameController.error = false;
_gameController.updater?.add(false);
_startUpdater();
},
child: const SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("The Reboot dll wasn't downloaded: disable your antivirus or proxy and click here to try again"
),
severity: InfoBarSeverity.info
)
),
),
);
}
}

View File

@@ -7,68 +7,44 @@ import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_instance.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import '../../model/update_status.dart';
const String kDefaultPlayerName = "Player";
class GameController extends GetxController {
late final GetStorage _storage;
late final TextEditingController username;
late final TextEditingController password;
late final RxBool showPassword;
late final TextEditingController customLaunchArgs;
late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion;
late final Rx<GameType> type;
late final HashMap<GameType, GameInstance> gameInstancesMap;
late final RxBool started;
late final RxBool autostartGameServer;
late bool updated;
late bool error;
late bool failing;
StreamController<bool>? updater;
late final Rx<UpdateStatus> updateStatus;
GameInstance? instance;
GameController() {
_storage = GetStorage("game");
Iterable decodedVersionsJson =
jsonDecode(_storage.read("versions") ?? "[]");
_storage = GetStorage("reboot_game");
Iterable decodedVersionsJson = jsonDecode(_storage.read("versions") ?? "[]");
var decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry))
.toList();
versions = Rx(decodedVersions);
versions.listen((data) => saveVersions());
versions.listen((data) => _saveVersions());
var decodedSelectedVersionName = _storage.read("version");
var decodedSelectedVersion = decodedVersions.firstWhereOrNull(
(element) => element.name == decodedSelectedVersionName);
_selectedVersion = Rxn(decodedSelectedVersion);
type = Rx(GameType.values.elementAt(_storage.read("type") ?? 0));
type.listen((value) {
_storage.write("type", value.index);
username.text = _readUsername();
});
username = TextEditingController(text: _readUsername());
username.addListener(() => _storage.write("${type.value == GameType.client ? 'game' : 'host'}_username", username.text));
username = TextEditingController(text: _storage.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage.write("username", username.text));
password = TextEditingController(text: _storage.read("password") ?? "");
password.addListener(() => _storage.write("password", password.text));
showPassword = RxBool(false);
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args" ?? ""));
customLaunchArgs.addListener(() => _storage.write("custom_launch_args", customLaunchArgs.text));
gameInstancesMap= HashMap();
started = RxBool(false);
autostartGameServer = RxBool(_storage.read("auto_start_game_server") ?? true);
autostartGameServer.listen((value) => _storage.write("auto_start_game_server", value));
updated = false;
error = false;
failing = false;
}
String _readUsername() {
var client = type.value == GameType.client;
return _storage.read("${client ? 'game' : 'host'}_username") ?? (client ? "" : "HostingServer");
updateStatus = Rx(UpdateStatus.waiting);
}
FortniteVersion? getVersionByName(String name) {
@@ -76,12 +52,15 @@ class GameController extends GetxController {
}
void addVersion(FortniteVersion version) {
var empty = versions.value.isEmpty;
versions.update((val) => val?.add(version));
if(empty){
selectedVersion = version;
}
}
FortniteVersion removeVersionByName(String versionName) {
var version =
versions.value.firstWhere((element) => element.name == versionName);
var version = versions.value.firstWhere((element) => element.name == versionName);
removeVersion(version);
return version;
}
@@ -90,9 +69,8 @@ class GameController extends GetxController {
versions.update((val) => val?.remove(version));
}
Future saveVersions() async {
var serialized =
jsonEncode(versions.value.map((entry) => entry.toJson()).toList());
Future<void> _saveVersions() async {
var serialized = jsonEncode(versions.value.map((entry) => entry.toJson()).toList());
await _storage.write("versions", serialized);
}
@@ -100,8 +78,6 @@ class GameController extends GetxController {
bool get hasNoVersions => versions.value.isEmpty;
GameInstance? get currentGameInstance => gameInstancesMap[type()];
FortniteVersion? get selectedVersion => _selectedVersion();
set selectedVersion(FortniteVersion? version) {

View File

@@ -0,0 +1,28 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../model/game_instance.dart';
const String kDefaultServerName = "Reboot Game Server";
class HostingController extends GetxController {
late final GetStorage _storage;
late final TextEditingController name;
late final TextEditingController category;
late final RxBool discoverable;
late final RxBool started;
GameInstance? instance;
HostingController() {
_storage = GetStorage("reboot_hosting");
name = TextEditingController(text: _storage.read("name") ?? kDefaultServerName);
name.addListener(() => _storage.write("name", name.text));
category = TextEditingController(text: _storage.read("category") ?? "");
category.addListener(() => _storage.write("category", category.text));
discoverable = RxBool(_storage.read("discoverable") ?? false);
discoverable.listen((value) => _storage.write("discoverable", value));
started = RxBool(false);
}
}

View File

@@ -5,8 +5,8 @@ import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:jaguar/jaguar.dart';
import '../model/server_type.dart';
import '../util/server.dart';
import '../../model/server_type.dart';
import '../../util/server.dart';
class ServerController extends GetxController {
static const String _serverName = "127.0.0.1";
@@ -18,14 +18,12 @@ class ServerController extends GetxController {
late final Rx<ServerType> type;
late final RxBool warning;
late RxBool started;
late RxBool loginAutomatically;
late RxBool detached;
HttpServer? remoteServer;
ServerController() {
_storage = GetStorage("server");
_storage = GetStorage("reboot_server");
started = RxBool(false);
loginAutomatically = RxBool(_storage.read("login_automatically") ?? false);
loginAutomatically.listen((value) => _storage.write("login_automatically", value));
type = Rx(ServerType.values.elementAt(_storage.read("type") ?? 0));
type.listen((value) {
host.text = _readHost();
@@ -43,6 +41,8 @@ class ServerController extends GetxController {
port.addListener(() => _storage.write("${type.value.id}_port", port.text));
warning = RxBool(_storage.read("lawin_value") ?? true);
warning.listen((value) => _storage.write("lawin_value", value));
detached = RxBool(_storage.read("detached") ?? false);
warning.listen((value) => _storage.write("detached", value));
}
String _readHost() {

View File

@@ -1,16 +1,18 @@
import 'dart:io';
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/main.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'dart:ui';
import '../util/reboot.dart';
import '../../util/reboot.dart';
class SettingsController extends GetxController {
static const String _kDefaultIp = "127.0.0.1";
static const bool _kDefaultAutoUpdate = true;
late final GetStorage _storage;
late final String originalDll;
late final TextEditingController updateUrl;
@@ -26,41 +28,30 @@ class SettingsController extends GetxController {
late double scrollingDistance;
SettingsController() {
_storage = GetStorage("settings");
_storage = GetStorage("reboot_settings");
updateUrl = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
updateUrl.addListener(() => _storage.write("update_url", updateUrl.text));
rebootDll = _createController("reboot", "reboot.dll");
consoleDll = _createController("console", "console.dll");
authDll = _createController("cranium2", "craniumv2.dll");
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
authDll = _createController("cobalt", "cobalt.dll");
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? _kDefaultIp);
matchmakingIp.addListener(() async {
var text = matchmakingIp.text;
_storage.write("ip", text);
writeMatchmakingIp(text);
});
width = _storage.read("width") ?? 912;
height = _storage.read("height") ?? 660;
width = _storage.read("width") ?? kDefaultWindowWidth;
height = _storage.read("height") ?? kDefaultWindowHeight;
offsetX = _storage.read("offset_x");
offsetY = _storage.read("offset_y");
autoUpdate = RxBool(_storage.read("auto_update") ?? false);
autoUpdate = RxBool(_storage.read("auto_update") ?? _kDefaultAutoUpdate);
autoUpdate.listen((value) async => _storage.write("auto_update", value));
scrollingDistance = 0.0;
}
TextEditingController _createController(String key, String name) {
loadBinary(name, true);
var controller = TextEditingController(text: _storage.read(key) ?? "${safeBinariesDirectory.path}\\$name");
var controller = TextEditingController(text: _storage.read(key) ?? _controllerDefaultPath(name));
controller.addListener(() => _storage.write(key, controller.text));
return controller;
}
@@ -73,4 +64,16 @@ class SettingsController extends GetxController {
_storage.write("offset_x", position.dx);
_storage.write("offset_y", position.dy);
}
void reset(){
updateUrl.text = rebootDownloadUrl;
rebootDll.text = _controllerDefaultPath("reboot.dll");
consoleDll.text = _controllerDefaultPath("console.dll");
authDll.text = _controllerDefaultPath("cobalt.dll");
matchmakingIp.text = _kDefaultIp;
writeMatchmakingIp(_kDefaultIp);
autoUpdate.value = _kDefaultAutoUpdate;
}
String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name";
}

View File

@@ -0,0 +1,6 @@
import 'package:get_storage/get_storage.dart';
final GetStorage _storage = GetStorage("reboot_update");
int? get updateTime => _storage.read("last_update_v2");
set updateTime(int? updateTime) => _storage.write("last_update_v2", updateTime);

View File

@@ -2,14 +2,14 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/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/ui/controller/game_controller.dart';
import '../util/checks.dart';
import '../../util/checks.dart';
import '../widget/shared/file_selector.dart';
import '../widget/shared/smart_check_box.dart';
import 'dialog.dart';
import 'dialog_button.dart';
class AddLocalVersion extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
@@ -24,9 +24,20 @@ class AddLocalVersion extends StatelessWidget {
return FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("Local builds are not guaranteed to work"),
severity: InfoBarSeverity.info
),
),
const SizedBox(
height: 16.0
),
TextFormBox(
controller: _nameController,
header: "Name",
@@ -35,19 +46,14 @@ class AddLocalVersion extends StatelessWidget {
validator: (text) => checkVersion(text, _gameController.versions.value)
),
const SizedBox(
height: 16.0
),
FileSelector(
label: "Path",
placeholder: "Type the game folder",
windowTitle: "Select game folder",
controller: _gamePathController,
validator: checkGameFolder,
folder: true
),
const SizedBox(height: 8.0),
)
],
),
buttons: [

View File

@@ -5,19 +5,20 @@ import 'package:async/async.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/util/error.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:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:universal_disk_space/universal_disk_space.dart';
import '../util/checks.dart';
import '../../util/checks.dart';
import '../controller/build_controller.dart';
import '../widget/home/build_selector.dart';
import '../widget/home/version_name_input.dart';
import '../widget/shared/file_selector.dart';
import 'dialog.dart';
import 'dialog_button.dart';
class AddServerVersion extends StatefulWidget {
const AddServerVersion({Key? key}) : super(key: key);
@@ -39,7 +40,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
DownloadStatus _status = DownloadStatus.form;
String _timeLeft = "00:00:00";
double _downloadProgress = 0;
Process? _manifestDownloadProcess;
CancelableOperation? _manifestDownloadProcess;
CancelableOperation? _driveDownloadOperation;
Object? _error;
StackTrace? _stackTrace;
@@ -49,7 +50,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
_fetchFuture = _buildController.builds != null
? Future.value(true)
: compute(fetchBuilds, null)
.then((value) => _buildController.builds = value);
.then((value) => _buildController.builds = value);
_diskSpace = DiskSpace();
_diskFuture = _diskSpace.scan()
.then((_) => _updateFormDefaults());
@@ -67,14 +68,12 @@ class _AddServerVersionState extends State<AddServerVersion> {
}
void _onDisposed() {
if (_status != DownloadStatus.downloading &&
_status != DownloadStatus.extracting) {
if (_status != DownloadStatus.downloading) {
return;
}
if (_manifestDownloadProcess != null) {
loadBinary("stop.bat", true).then(
(value) => Process.runSync(value.path, []));
_manifestDownloadProcess?.cancel();
_buildController.cancelledDownload(true);
return;
}
@@ -129,13 +128,14 @@ class _AddServerVersionState extends State<AddServerVersion> {
void _startDownload(BuildContext context) async {
try {
setState(() => _status = DownloadStatus.downloading);
_manifestDownloadProcess = await downloadManifestBuild(
var future = downloadArchiveBuild(
_buildController.selectedBuild.link,
_pathController.text,
_onDownloadProgress
Directory(_pathController.text),
_onDownloadProgress,
_onUnrar
);
_manifestDownloadProcess!.exitCode
.then((value) => _onDownloadComplete());
future.then((value) => _onDownloadComplete());
_manifestDownloadProcess = CancelableOperation.fromFuture(future);
} catch (exception, stackTrace) {
_onDownloadError(exception, stackTrace);
}
@@ -145,7 +145,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
setState(() => _status = DownloadStatus.extracting);
}
void _onDownloadComplete() {
Future<void> _onDownloadComplete() async {
if (!mounted) {
return;
}
@@ -160,6 +160,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
}
void _onDownloadError(Object? error, StackTrace? stackTrace) {
print("Error");
if (!mounted) {
return;
}
@@ -183,59 +184,78 @@ class _AddServerVersionState extends State<AddServerVersion> {
});
}
Padding _createExtractingBody() {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: InfoLabel(
label: "Extracting...",
child: const SizedBox(width: double.infinity, child: ProgressBar())),
);
}
Widget _createDownloadBody() => Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"Downloading...",
style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start,
),
),
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,
),
const SizedBox(
height: 8.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${_downloadProgress.round()}%",
style: FluentTheme.maybeOf(context)?.typography.body,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${_downloadProgress.round()}%",
style: FluentTheme.maybeOf(context)?.typography.body,
),
Text(
"Time left: $_timeLeft",
style: FluentTheme.maybeOf(context)?.typography.body,
)
],
),
Text(
"Time left: $_timeLeft",
style: FluentTheme.maybeOf(context)?.typography.body,
)
],
),
const SizedBox(
height: 8,
),
const SizedBox(
height: 8.0,
),
SizedBox(
width: double.infinity,
child: ProgressBar(value: _downloadProgress.toDouble())),
const SizedBox(
height: 16,
)
],
);
}
SizedBox(
width: double.infinity,
child: ProgressBar(value: _downloadProgress.toDouble())
),
const SizedBox(
height: 8.0,
)
],
);
Widget _createExtractingBody() => Column(
mainAxisSize: MainAxisSize.min,
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
"Extracting...",
style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start,
),
),
const SizedBox(
height: 8.0,
),
const SizedBox(
width: double.infinity,
child: ProgressBar()
),
const SizedBox(
height: 8.0,
)
],
);
Widget _createFormDialog() {
return FutureBuilder(
@@ -265,21 +285,19 @@ class _AddServerVersionState extends State<AddServerVersion> {
Widget _createFormBody() {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const BuildSelector(),
const SizedBox(height: 16.0),
const SizedBox(height: 20.0),
VersionNameInput(controller: _nameController),
const SizedBox(height: 16.0),
FileSelector(
label: "Path",
placeholder: "Type the download destination",
windowTitle: "Select download destination",
controller: _pathController,
validator: checkDownloadDestination,
folder: true
),
const SizedBox(height: 8.0),
],
);
}

View File

@@ -2,7 +2,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
import 'dialog_button.dart';

View File

@@ -1,8 +1,8 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import '../../main.dart';
import '../../../main.dart';
import 'dialog.dart';
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
"This means that you cannot currently host this version of the game. "

View File

@@ -1,32 +1,30 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.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/model/server_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
import 'package:sync/semaphore.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../main.dart';
import '../util/server.dart';
import '../../../main.dart';
import '../../util/server.dart';
import '../controller/server_controller.dart';
import 'dialog.dart';
import 'dialog_button.dart';
extension ServerControllerDialog on ServerController {
static Semaphore semaphore = Semaphore();
Future<bool> restart() async {
Future<bool> restart(bool closeLocalPromptAutomatically) async {
await resetWinNat();
return (!started() || await stop()) && await toggle();
return (!started() || await stop()) && await toggle(closeLocalPromptAutomatically);
}
Future<bool> toggle() async {
Future<bool> toggle(bool closeLocalPromptAutomatically) async {
try{
semaphore.acquire();
if (type() == ServerType.local) {
return _pingSelfInteractive();
return _pingSelfInteractive(closeLocalPromptAutomatically);
}
var result = await _toggle();
@@ -35,7 +33,7 @@ extension ServerControllerDialog on ServerController {
return false;
}
var ping = await _pingSelfInteractive();
var ping = await _pingSelfInteractive(true);
if(!ping){
started.value = false;
return false;
@@ -79,7 +77,7 @@ extension ServerControllerDialog on ServerController {
try{
switch(type()){
case ServerType.embedded:
startServer();
startServer(detached());
break;
case ServerType.remote:
var uriResult = await _pingRemoteInteractive();
@@ -166,15 +164,27 @@ extension ServerControllerDialog on ServerController {
}
}
Future<bool> _pingSelfInteractive() async {
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
try {
var resultFuture = compute(pingSelf, port.text)
.then((value) => value != null);
await showDialog<bool>(
Future<bool> ping() async {
for(var i = 0; i < 3; i++){
var result = await pingSelf(port.text);
if(result != null){
return true;
}else {
await Future.delayed(const Duration(seconds: 1));
}
}
return false;
}
var future = _waitFutureOrTime(ping());
var result = await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: _waitFutureOrTime(resultFuture),
future: future,
loadingMessage: "Pinging ${type().id} server...",
successfulBody: FutureBuilderDialog.ofMessage(
"The ${type().id} server works correctly"),
@@ -182,10 +192,10 @@ extension ServerControllerDialog on ServerController {
"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: true
closeAutomatically: closeAutomatically
)
);
return await resultFuture;
) ?? false;
return result && await future;
} catch (_) {
return false;
}
@@ -193,12 +203,14 @@ extension ServerControllerDialog on ServerController {
Future<Uri?> _pingRemoteInteractive() async {
try {
var mainFuture = ping(host.text, port.text);
await showDialog<bool>(
var mainFuture = ping(host.text, port.text).then((value) => value != null);
var future = _waitFutureOrTime(mainFuture);
var result = await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: _waitFutureOrTime(mainFuture.then((value) => value != null)),
future: future,
closeAutomatically: false,
loadingMessage: "Pinging remote server...",
successfulBody: FutureBuilderDialog.ofMessage(
"The server at ${host.text}:${port
@@ -209,7 +221,7 @@ extension ServerControllerDialog on ServerController {
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
)
) ?? false;
return await mainFuture;
return result ? await future : null;
} catch (_) {
return null;
}
@@ -259,60 +271,48 @@ extension ServerControllerDialog on ServerController {
);
}
void showUnexpectedServerError() {
showDialog(
context: appKey.currentContext!,
builder: (context) => InfoDialog(
text: "The backend server died unexpectedly",
buttons: [
DialogButton(
text: "Close",
type: ButtonType.secondary,
onTap: () => Navigator.of(context).pop(),
),
void showUnexpectedServerError() => showDialog(
context: appKey.currentContext!,
builder: (context) => InfoDialog(
text: "The backend server died unexpectedly",
buttons: [
DialogButton(
text: "Close",
type: ButtonType.secondary,
onTap: () => Navigator.of(context).pop(),
),
DialogButton(
DialogButton(
text: "Open log",
type: ButtonType.primary,
onTap: () {
launchUrl(serverLogFile.uri);
if(serverLogFile.existsSync()){
showMessage("No log is available");
}else {
launchUrl(serverLogFile.uri);
}
Navigator.of(context).pop();
}
),
],
)
);
}
),
],
)
);
void _showIllegalPortError() {
showMessage("Illegal port for backend server, use only numbers");
}
void _showIllegalPortError() => showMessage("Illegal port for backend server, use only numbers");
void _showMissingPortError() {
showMessage("Missing port for backend server");
}
void _showMissingPortError() => showMessage("Missing port for backend server");
void _showMissingHostError() {
showMessage("Missing the host name for backend server");
}
}
void _showMissingHostError() => showMessage("Missing the host name for backend server");
Future<Object?> _showUnknownError(ServerResult result) {
return showDialog(
context: appKey.currentContext!,
builder: (context) =>
ErrorDialog(
exception: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace,
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
)
);
}
Future<Object?> _showUnknownError(ServerResult result) => showDialog(
context: appKey.currentContext!,
builder: (context) =>
ErrorDialog(
exception: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace,
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
)
);
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) {
return Future.wait<bool>([
resultFuture,
Future.delayed(const Duration(seconds: 1))
.then((value) => true)
]).then((value) => value.reduce((f, s) => f && s));
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) => Future.wait<bool>([resultFuture, Future.delayed(const Duration(seconds: 1)).then((value) => true)]).then((value) => value.reduce((f, s) => f && s));
}

View File

@@ -1,7 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import '../../main.dart';
import '../page/home_page.dart';
import '../../../main.dart';
void showMessage(String text){
showSnackbar(

View File

@@ -2,21 +2,16 @@ 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/model/game_type.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';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/os/window_border.dart';
import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
import 'package:reboot_launcher/src/ui/page/launcher_page.dart';
import 'package:reboot_launcher/src/ui/page/server_page.dart';
import 'package:reboot_launcher/src/ui/page/settings_page.dart';
import 'package:window_manager/window_manager.dart';
import '../controller/settings_controller.dart';
import '../model/server_type.dart';
import '../widget/os/window_border.dart';
import '../widget/os/window_buttons.dart';
import 'hosting_page.dart';
import 'info_page.dart';
class HomePage extends StatefulWidget {
@@ -29,9 +24,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WindowListener {
static const double _defaultPadding = 12.0;
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();
@@ -39,8 +32,8 @@ class _HomePageState extends State<HomePage> with WindowListener {
final Rxn<List<NavigationPaneItem>> _searchItems = Rxn();
final RxBool _focused = RxBool(true);
final RxInt _index = RxInt(0);
bool _navigated = false;
bool _shouldMaximize = false;
final RxBool _nestedNavigation = RxBool(false);
final GlobalKey<NavigatorState> _settingsNavigatorKey = GlobalKey();
@override
void initState() {
@@ -91,35 +84,6 @@ class _HomePageState extends State<HomePage> with WindowListener {
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) => Obx(() => Stack(
children: [
@@ -130,7 +94,8 @@ class _HomePageState extends State<HomePage> with WindowListener {
),
appBar: NavigationAppBar(
title: _draggableArea,
actions: WindowTitleBar(focused: _focused())
actions: WindowTitleBar(focused: _focused()),
leading: _backButton
),
pane: NavigationPane(
selected: _selectedIndex,
@@ -149,11 +114,34 @@ class _HomePageState extends State<HomePage> with WindowListener {
]
));
void _onIndexChanged(int index) {
_index.value = index;
_navigated = true;
Widget get _backButton => Obx(() {
// ignore: unused_local_variable
var ignored = _nestedNavigation.value;
return PaneItem(
icon: const Icon(FluentIcons.back, size: 14.0),
body: const SizedBox.shrink(),
).build(
context,
false,
_onBack(),
displayMode: PaneDisplayMode.compact
);
});
void Function()? _onBack() {
var navigator = _settingsNavigatorKey.currentState;
if(navigator == null || !navigator.mounted || !navigator.canPop()){
return null;
}
return () async {
Navigator.pop(navigator.context);
_nestedNavigation.value = false;
};
}
void _onIndexChanged(int index) => _index.value = index;
TextBox get _autoSuggestBox => TextBox(
key: _searchKey,
controller: _searchController,
@@ -162,15 +150,7 @@ class _HomePageState extends State<HomePage> with WindowListener {
);
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: () {
if(!_shouldMaximize){
return;
}
appWindow.maximizeOrRestore();
_shouldMaximize = false;
},
onDoubleTapDown: (details) => _shouldMaximize = true,
onDoubleTap: () => appWindow.maximizeOrRestore(),
onHorizontalDragStart: (event) => appWindow.startDragging(),
onVerticalDragStart: (event) => appWindow.startDragging()
);
@@ -205,32 +185,29 @@ class _HomePageState extends State<HomePage> with WindowListener {
List<NavigationPaneItem> get _items => _searchItems() ?? [
PaneItem(
title: const Text("Home"),
title: const Text("Play"),
icon: const Icon(FluentIcons.game),
body: const LauncherPage()
),
PaneItem(
title: const Text("Host"),
icon: const Icon(FluentIcons.server_processes),
body: const HostingPage()
),
PaneItem(
title: const Text("Backend"),
icon: const Icon(FluentIcons.server_enviroment),
icon: const Icon(FluentIcons.user_window),
body: ServerPage()
),
PaneItem(
title: const Text("Tutorial"),
icon: const Icon(FluentIcons.info),
body: const InfoPage(),
onTap: _onTutorial
body: InfoPage(_settingsNavigatorKey, _nestedNavigation)
),
];
void _onTutorial() {
if(!_navigated){
setState(() => _settingsController.scrollingDistance = 0);
}
_navigated = false;
}
String get searchValue => _searchController.text;
}

View File

@@ -0,0 +1,105 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
class HostingPage extends StatefulWidget {
const HostingPage(
{Key? key})
: super(key: key);
@override
State<HostingPage> createState() => _HostingPageState();
}
class _HostingPageState extends State<HostingPage> {
final HostingController _hostingController = Get.find<HostingController>();
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
const SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("A window will pop up after the game server is started to modify its in-game settings"),
severity: InfoBarSeverity.info
),
),
const SizedBox(
height: 16.0
),
SettingTile(
title: "Game Server",
subtitle: "Provide basic information about your server",
expandedContentSpacing: 0,
expandedContent: [
SettingTile(
title: "Name",
subtitle: "The name of your game server",
isChild: true,
content: TextFormBox(
placeholder: "Name",
controller: _hostingController.name
)
),
SettingTile(
title: "Category",
subtitle: "The category of your game server",
isChild: true,
content: TextFormBox(
placeholder: "Category",
controller: _hostingController.category
)
),
SettingTile(
title: "Discoverable",
subtitle: "Make your server available to other players on the server browser",
isChild: true,
contentWidth: null,
content: Obx(() => ToggleSwitch(
checked: _hostingController.discoverable(),
onChanged: (value) => _hostingController.discoverable.value = value
))
),
],
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Version",
subtitle: "Select the version of Fortnite you want to host",
content: const VersionSelector(),
expandedContent: [
SettingTile(
title: "Add a version from this PC's local storage",
subtitle: "Versions coming from your local disk are not guaranteed to work",
content: Button(
onPressed: () => VersionSelector.openAddDialog(context),
child: const Text("Add build"),
),
isChild: true
),
SettingTile(
title: "Download any version from the cloud",
subtitle: "A curated list of supported versions by Project Reboot",
content: Button(
onPressed: () => VersionSelector.openDownloadDialog(context),
child: const Text("Download"),
),
isChild: true
)
]
),
const Expanded(child: SizedBox()),
const LaunchButton(
host: true
)
],
);
}

View File

@@ -1,12 +1,15 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../controller/settings_controller.dart';
import '../widget/shared/fluent_card.dart';
class InfoPage extends StatefulWidget {
const InfoPage({Key? key}) : super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final RxBool nestedNavigation;
const InfoPage(this.navigatorKey, this.nestedNavigation, {Key? key}) : super(key: key);
@override
State<InfoPage> createState() => _InfoPageState();
@@ -36,7 +39,6 @@ class _InfoPageState extends State<InfoPage> {
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
];
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
final SettingsController _settingsController = Get.find<SettingsController>();
late final ScrollController _controller;
@@ -57,7 +59,7 @@ class _InfoPageState extends State<InfoPage> {
@override
Widget build(BuildContext context) => Navigator(
key: _navigatorKey,
key: widget.navigatorKey,
initialRoute: "home",
onGenerateRoute: (settings) {
var screen = _createScreen(settings.name);
@@ -69,6 +71,8 @@ class _InfoPageState extends State<InfoPage> {
);
Widget _createScreen(String? name) {
WidgetsBinding.instance
.addPostFrameCallback((_) => widget.nestedNavigation.value = name != "home");
switch(name){
case "home":
return _homeScreen;
@@ -87,7 +91,7 @@ class _InfoPageState extends State<InfoPage> {
_createCardWidget(
text: "Play on someone else's server",
description: "If one of your friends is hosting a game server, click here",
onClick: () => _navigatorKey.currentState?.pushNamed("else")
onClick: () => widget.navigatorKey.currentState?.pushNamed("else")
),
const SizedBox(
@@ -97,7 +101,7 @@ class _InfoPageState extends State<InfoPage> {
_createCardWidget(
text: "Host your own server",
description: "If you want to create your own server to invite your friends or to play around by yourself, click here",
onClick: () => _navigatorKey.currentState?.pushNamed("own")
onClick: () => widget.navigatorKey.currentState?.pushNamed("own")
)
]
);

View File

@@ -0,0 +1,216 @@
import 'dart:async';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart';
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/ui/widget/home/version_selector.dart';
import 'package:reboot_launcher/src/ui/widget/shared/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../model/update_status.dart';
import '../../util/checks.dart';
import '../../util/reboot.dart';
import '../controller/update_controller.dart';
class LauncherPage extends StatefulWidget {
const LauncherPage(
{Key? key})
: super(key: key);
@override
State<LauncherPage> createState() => _LauncherPageState();
}
class _LauncherPageState extends State<LauncherPage> {
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final BuildController _buildController = Get.find<BuildController>();
late final RxBool _showPasswordTrailing = RxBool(_gameController.password.text.isNotEmpty);
@override
void initState() {
if(_gameController.updateStatus() == UpdateStatus.waiting) {
_startUpdater();
_setupBuildWarning();
}
super.initState();
}
void _setupBuildWarning() {
void onCancelWarning() => WidgetsBinding.instance.addPostFrameCallback((_) {
if(!mounted) {
return;
}
showSnackbar(context, const Snackbar(content: Text("Download cancelled")));
_buildController.cancelledDownload(false);
});
_buildController.cancelledDownload.listen((value) => value ? onCancelWarning() : {});
}
Future<void> _startUpdater() async {
if(!_settingsController.autoUpdate()){
_gameController.updateStatus.value = UpdateStatus.success;
return;
}
_gameController.updateStatus.value = UpdateStatus.started;
try {
updateTime = await downloadRebootDll(_settingsController.updateUrl.text, updateTime);
_gameController.updateStatus.value = UpdateStatus.success;
}catch(_) {
_gameController.updateStatus.value = UpdateStatus.error;
rethrow;
}
}
@override
Widget build(BuildContext context) => Obx(() => !_settingsController.autoUpdate() || _gameController.updateStatus().isDone() ? _homePage : _updateScreen);
Widget get _homePage => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _gameController.updateStatus() == UpdateStatus.error ? _updateError : const SizedBox(),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
child: SizedBox(height: _gameController.updateStatus() == UpdateStatus.error ? 16.0 : 0.0),
),
SettingTile(
title: "Credentials",
subtitle: "Your in-game login credentials",
expandedContentSpacing: 0,
expandedContent: [
SettingTile(
title: "Username",
subtitle: "The username that other players will see when you are in game",
isChild: true,
content: TextFormBox(
placeholder: "Username",
controller: _gameController.username,
autovalidateMode: AutovalidateMode.always
),
),
SettingTile(
title: "Password",
subtitle: "The password of your account, only used if the backend requires it",
isChild: true,
content: Obx(() => TextFormBox(
placeholder: "Password",
controller: _gameController.password,
autovalidateMode: AutovalidateMode.always,
obscureText: !_gameController.showPassword.value,
enableSuggestions: false,
autocorrect: false,
onChanged: (text) => _showPasswordTrailing.value = text.isNotEmpty,
suffix: Button(
onPressed: () => _gameController.showPassword.value = !_gameController.showPassword.value,
style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
_gameController.showPassword.value ? Icons.visibility_off : Icons.visibility,
color: _showPasswordTrailing.value ? null : Colors.transparent
),
)
))
)
],
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Matchmaking host",
subtitle: "Enter the IP address of the game server hosting the match",
content: TextFormBox(
placeholder: "IP:PORT",
controller: _settingsController.matchmakingIp,
validator: checkMatchmaking,
autovalidateMode: AutovalidateMode.always
),
expandedContent: [
SettingTile(
title: "Browse available servers",
subtitle: "Discover new game servers that fit your play-style",
content: Button(
onPressed: () => launchUrl(Uri.parse("https://google.com/search?q=One+Day+This+Will+Be+Ready")),
child: const Text("Browse")
),
isChild: true
)
]
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Version",
subtitle: "Select the version of Fortnite you want to play",
content: const VersionSelector(),
expandedContent: [
SettingTile(
title: "Add a version from this PC's local storage",
subtitle: "Versions coming from your local disk are not guaranteed to work",
content: Button(
onPressed: () => VersionSelector.openAddDialog(context),
child: const Text("Add build"),
),
isChild: true
),
SettingTile(
title: "Download any version from the cloud",
subtitle: "A curated list of supported versions by Project Reboot",
content: Button(
onPressed: () => VersionSelector.openDownloadDialog(context),
child: const Text("Download"),
),
isChild: true
)
]
),
const Expanded(child: SizedBox()),
const LaunchButton(
host: false
)
],
);
Widget get _updateScreen => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
],
);
Widget get _updateError => MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _startUpdater(),
child: const SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("The reboot dll couldn't be downloaded: click here to try again"),
severity: InfoBarSeverity.info
)
),
),
);
}

View File

@@ -1,12 +1,13 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/widget/server/host_input.dart';
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/server/port_input.dart';
import 'package:reboot_launcher/src/widget/server/server_button.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
import 'package:reboot_launcher/src/ui/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/ui/widget/server/server_button.dart';
import 'package:url_launcher/url_launcher.dart';
import '../model/server_type.dart';
import '../widget/shared/setting_tile.dart';
class ServerPage extends StatelessWidget {
@@ -33,7 +34,7 @@ class ServerPage extends StatelessWidget {
title: "Host",
subtitle: "Enter the host of the backend server",
content: TextFormBox(
placeholder: "host",
placeholder: "Host",
controller: _serverController.host,
enabled: _isRemote
)
@@ -42,10 +43,10 @@ class ServerPage extends StatelessWidget {
height: 16.0,
),
SettingTile(
title: "Host",
title: "Port",
subtitle: "Enter the port of the backend server",
content: TextFormBox(
placeholder: "host",
placeholder: "Port",
controller: _serverController.port,
enabled: _isRemote
)
@@ -54,24 +55,32 @@ class ServerPage extends StatelessWidget {
height: 16.0,
),
SettingTile(
title: "Type",
subtitle: "Select the type of backend to use",
content: ServerTypeSelector()
title: "Type",
subtitle: "Select the type of backend to use",
content: ServerTypeSelector()
),
const SizedBox(
height: 16.0,
),
Align(
alignment: Alignment.bottomCenter,
child: SettingTile(
title: "Login automatically",
subtitle: "Choose whether the game client should login automatically using random credentials",
SettingTile(
title: "Detached",
subtitle: "Choose whether the backend should be started as a separate process, useful for debugging",
contentWidth: null,
content: Obx(() => ToggleSwitch(
checked: _serverController.loginAutomatically(),
onChanged: (value) => _serverController.loginAutomatically.value = value
checked: _serverController.detached(),
onChanged: (value) => _serverController.detached.value = value
))
),
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Server files",
subtitle: "The location where the backend is stored",
content: Button(
onPressed: () => launchUrl(serverDirectory.uri),
child: const Text("Open")
)
),
const Expanded(child: SizedBox()),
const ServerButton()

View File

@@ -1,13 +1,16 @@
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/settings_controller.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/dialog_button.dart';
import 'package:url_launcher/url_launcher.dart';
import '../util/checks.dart';
import '../../util/checks.dart';
import '../../util/os.dart';
import '../../util/selector.dart';
import '../dialog/dialog.dart';
import '../widget/shared/setting_tile.dart';
class SettingsPage extends StatelessWidget {
@@ -60,7 +63,7 @@ class SettingsPage extends StatelessWidget {
title: "Custom launch arguments",
subtitle: "Enter additional arguments to use when launching the game",
content: TextFormBox(
placeholder: "args",
placeholder: "Arguments...",
controller: _gameController.customLaunchArgs,
)
),
@@ -71,10 +74,51 @@ class SettingsPage extends StatelessWidget {
title: "Create a bug report",
subtitle: "Help me fix bugs by reporting them",
content: Button(
onPressed: () => launchUrl(Uri.parse("https://discord.com/channels/998020695223193670/1031262639457828910")),
onPressed: () => launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/issues/new/choose")),
child: const Text("Report a bug"),
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Reset settings",
subtitle: "Resets the launcher's settings to their default values",
content: Button(
onPressed: () => showDialog(
context: context,
builder: (context) => InfoDialog(
text: "Do you want to reset all settings to their default values? This action is irreversible",
buttons: [
DialogButton(
type: ButtonType.secondary,
text: "Close",
),
DialogButton(
type: ButtonType.primary,
text: "Reset",
onTap: () {
_settingsController.reset();
Navigator.of(context).pop();
},
)
],
)
),
child: const Text("Reset"),
)
),
const SizedBox(
height: 16.0,
),
SettingTile(
title: "Version status",
subtitle: "Current version: 7.0",
content: Button(
onPressed: () => launchUrl(installationDirectory.uri),
child: const Text("Show Files"),
)
),
]
);
@@ -87,7 +131,7 @@ class SettingsPage extends StatelessWidget {
children: [
Expanded(
child: TextFormBox(
placeholder: "path",
placeholder: "Path",
controller: controller,
validator: checkDll,
autovalidateMode: AutovalidateMode.always
@@ -99,7 +143,10 @@ class SettingsPage extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: () { },
onPressed: () async {
var selected = await compute(openFilePicker, "dll");
controller.text = selected ?? controller.text;
},
child: const Icon(FluentIcons.open_folder_horizontal),
),
)

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/ui/controller/build_controller.dart';
import 'package:reboot_launcher/src/model/fortnite_build.dart';
class BuildSelector extends StatefulWidget {

View File

@@ -1,36 +1,36 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
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/game_dialogs.dart';
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
import 'package:reboot_launcher/src/ui/dialog/game_dialogs.dart';
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/../main.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/ui/controller/settings_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
import 'package:reboot_launcher/src/model/game_instance.dart';
import '../../util/process.dart';
import '../../../util/process.dart';
class LaunchButton extends StatefulWidget {
const LaunchButton(
{Key? key})
: super(key: key);
final bool host;
const LaunchButton({Key? key, required this.host}) : super(key: key);
@override
State<LaunchButton> createState() => _LaunchButtonState();
@@ -51,17 +51,12 @@ class _LaunchButtonState extends State<LaunchButton> {
];
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>();
File? _logFile;
final File _logFile = File("${assetsDirectory.path}\\logs\\game.log");
bool _fail = false;
@override
void initState() {
loadBinary("game.txt", true)
.then((value) => _logFile = value);
super.initState();
}
Future? _executor;
@override
Widget build(BuildContext context) => Align(
@@ -74,26 +69,34 @@ class _LaunchButtonState extends State<LaunchButton> {
child: Align(
alignment: Alignment.center,
child: Text(
_gameController.started() ? "Close fortnite" : "Launch fortnite"
_hasStarted ? _stopMessage : _startMessage
),
),
onPressed: () => _start(_gameController.type()),
onPressed: () => _executor = _start()
),
)),
),
);
void _start(GameType type) async {
if (_gameController.started()) {
_onStop(type);
bool get _hasStarted => widget.host ? _hostingController.started() : _gameController.started();
void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started;
String get _startMessage => widget.host ? "Start hosting" : "Launch fortnite";
String get _stopMessage => widget.host ? "Stop hosting" : "Close fortnite";
Future<void> _start() async {
if (_hasStarted) {
_onStop(widget.host);
return;
}
_gameController.started.value = true;
_setStarted(widget.host, true);
if (_gameController.username.text.isEmpty) {
if(_serverController.type() != ServerType.local){
showMessage("Missing username");
_onStop(type);
_onStop(widget.host);
return;
}
@@ -102,31 +105,29 @@ class _LaunchButtonState extends State<LaunchButton> {
if (_gameController.selectedVersion == null) {
showMessage("No version is selected");
_onStop(type);
_onStop(widget.host);
return;
}
for (var element in Injectable.values) {
if(await _getDllPath(element, type) == null) {
if(await _getDllPath(element, widget.host) == null) {
return;
}
}
try {
_fail = false;
await _resetLogFile();
var version = _gameController.selectedVersion!;
var gamePath = version.executable?.path;
if(gamePath == null){
showMissingBuildError(version);
_onStop(type);
_onStop(widget.host);
return;
}
var result = _serverController.started() || await _serverController.toggle();
var result = _serverController.started() || await _serverController.toggle(true);
if(!result){
_onStop(type);
_onStop(widget.host);
return;
}
@@ -134,28 +135,34 @@ class _LaunchButtonState extends State<LaunchButton> {
await compute(patchHeadless, version.executable!);
var automaticallyStartedServer = await _startMatchMakingServer();
await _startGameProcesses(version, type, automaticallyStartedServer);
await _startGameProcesses(version, widget.host, automaticallyStartedServer);
if(type == GameType.headlessServer){
if(widget.host){
await _showServerLaunchingWarning();
}
} catch (exception, stacktrace) {
_closeDialogIfOpen(false);
showCorruptedBuildError(type != GameType.client, exception, stacktrace);
_onStop(type);
showCorruptedBuildError(widget.host, exception, stacktrace);
_onStop(widget.host);
}
}
Future<void> _startGameProcesses(FortniteVersion version, GameType type, bool hasChildServer) async {
Future<void> _startGameProcesses(FortniteVersion version, bool host, bool hasChildServer) async {
_setStarted(host, true);
var launcherProcess = await _createLauncherProcess(version);
var eacProcess = await _createEacProcess(version);
var gameProcess = await _createGameProcess(version.executable!.path, type);
_gameController.gameInstancesMap[type] = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
_injectOrShowError(Injectable.cranium, type);
var gameProcess = await _createGameProcess(version.executable!.path, host);
var instance = GameInstance(gameProcess, launcherProcess, eacProcess, hasChildServer);
if(host){
_hostingController.instance = instance;
}else{
_gameController.instance = instance;
}
_injectOrShowError(Injectable.sslBypass, host);
}
Future<bool> _startMatchMakingServer() async {
if(_gameController.type() != GameType.client){
if(widget.host){
return false;
}
@@ -164,33 +171,32 @@ class _LaunchButtonState extends State<LaunchButton> {
return false;
}
if(!_gameController.autostartGameServer()){
return false;
}
var version = _gameController.selectedVersion!;
await _startGameProcesses(
version,
GameType.headlessServer,
false
);
await _startGameProcesses(version, true, false);
return true;
}
Future<Process> _createGameProcess(String gamePath, GameType type) async {
var gameArgs = createRebootArgs(_gameController.username.text, type, _gameController.customLaunchArgs.text);
Future<Process> _createGameProcess(String gamePath, bool host) async {
var gameArgs = createRebootArgs(_safeUsername, host, _gameController.customLaunchArgs.text);
var gameProcess = await Process.start(gamePath, gameArgs);
gameProcess
..exitCode.then((_) => _onEnd(type))
..outLines.forEach((line) => _onGameOutput(line, type))
..errLines.forEach((line) => _onGameOutput(line, type));
..exitCode.then((_) => _onEnd())
..outLines.forEach((line) => _onGameOutput(line, host))
..errLines.forEach((line) => _onGameOutput(line, host));
return gameProcess;
}
Future<void> _resetLogFile() async {
if(_logFile != null && await _logFile!.exists()){
await _logFile!.delete();
String get _safeUsername {
if (_gameController.username.text.isEmpty) {
return kDefaultPlayerName;
}
var username = _gameController.username.text.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}
Future<Process?> _createLauncherProcess(FortniteVersion version) async {
@@ -215,13 +221,13 @@ class _LaunchButtonState extends State<LaunchButton> {
return eacProcess;
}
void _onEnd(GameType type) {
void _onEnd() {
if(_fail){
return;
}
_closeDialogIfOpen(false);
_onStop(type);
_onStop(widget.host);
}
void _closeDialogIfOpen(bool success) {
@@ -238,7 +244,7 @@ class _LaunchButtonState extends State<LaunchButton> {
context: appKey.currentContext!,
builder: (context) => ProgressDialog(
text: "Launching headless server...",
onStop: () =>_onEnd(_gameController.type())
onStop: () =>_onEnd()
)
) ?? false;
@@ -246,16 +252,14 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
_onStop(_gameController.type());
_onStop(widget.host);
}
void _onGameOutput(String line, GameType type) {
if(_logFile != null){
_logFile!.writeAsString("$line\n", mode: FileMode.append);
}
void _onGameOutput(String line, bool host) {
_logFile.createSync(recursive: true);
_logFile.writeAsString("$line\n", mode: FileMode.append);
if (line.contains(_shutdownLine)) {
_onStop(type);
_onStop(host);
return;
}
@@ -265,8 +269,8 @@ class _LaunchButtonState extends State<LaunchButton> {
}
_fail = true;
showCorruptedBuildError(type != GameType.client);
_onStop(type);
showCorruptedBuildError(host);
_onStop(host);
return;
}
@@ -277,71 +281,76 @@ class _LaunchButtonState extends State<LaunchButton> {
_fail = true;
_closeDialogIfOpen(false);
_showTokenError(type);
_showTokenError(host);
return;
}
if(line.contains("Region ")){
if(type == GameType.client){
_injectOrShowError(Injectable.console, type);
if(!host){
_injectOrShowError(Injectable.console, host);
}else {
_injectOrShowError(Injectable.reboot, type)
_injectOrShowError(Injectable.reboot, host)
.then((value) => _closeDialogIfOpen(true));
}
_injectOrShowError(Injectable.memoryFix, type);
_gameController.currentGameInstance?.tokenError = false;
_injectOrShowError(Injectable.memoryFix, host);
var instance = host ? _hostingController.instance : _gameController.instance;
instance?.tokenError = false;
}
}
Future<void> _showTokenError(GameType type) async {
Future<void> _showTokenError(bool host) async {
var instance = host ? _hostingController.instance : _gameController.instance;
if(_serverController.type() != ServerType.embedded) {
showTokenErrorUnfixable();
_gameController.currentGameInstance?.tokenError = true;
instance?.tokenError = true;
return;
}
var tokenError = _gameController.currentGameInstance?.tokenError;
_gameController.currentGameInstance?.tokenError = true;
await _serverController.restart();
var tokenError = instance?.tokenError;
instance?.tokenError = true;
await _serverController.restart(true);
if (tokenError == true) {
showTokenErrorCouldNotFix();
return;
}
showTokenErrorFixable();
_onStop(type);
_start(type);
_onStop(host);
_start();
}
void _onStop(GameType? type) {
if(type == null){
return;
void _onStop(bool host) async {
if(_executor != null){
await _executor;
}
var value = _gameController.gameInstancesMap[type];
if(value != null){
if(value.hasChildServer){
_onStop(GameType.headlessServer);
var instance = host ? _hostingController.instance : _gameController.instance;
if(instance != null){
if(instance.hasChildServer){
_onStop(true);
}
value.kill();
_gameController.gameInstancesMap.remove(type);
instance.kill();
if(host){
_hostingController.instance = null;
}else {
_gameController.instance = null;
}
}
if(type == _gameController.type()) {
_gameController.started.value = false;
}
_setStarted(host, false);
}
Future<void> _injectOrShowError(Injectable injectable, GameType type) async {
var gameProcess = _gameController.gameInstancesMap[type]?.gameProcess;
if (gameProcess == null) {
Future<void> _injectOrShowError(Injectable injectable, bool hosting) async {
var instance = hosting ? _hostingController.instance : _gameController.instance;
if (instance == null) {
return;
}
try {
var dllPath = await _getDllPath(injectable, type);
var gameProcess = instance.gameProcess;
var dllPath = await _getDllPath(injectable, hosting);
if(dllPath == null) {
return;
}
@@ -349,21 +358,21 @@ class _LaunchButtonState extends State<LaunchButton> {
await injectDll(gameProcess.pid, dllPath.path);
} catch (exception) {
showMessage("Cannot inject $injectable.dll: $exception");
_onStop(type);
_onStop(hosting);
}
}
Future<File?> _getDllPath(Injectable injectable, GameType type) async {
Future<File?> _getDllPath(Injectable injectable, bool hosting) async {
Future<File> getPath(Injectable injectable) async {
switch(injectable){
case Injectable.reboot:
return File(_settingsController.rebootDll.text);
case Injectable.console:
return File(_settingsController.consoleDll.text);
case Injectable.cranium:
case Injectable.sslBypass:
return File(_settingsController.authDll.text);
case Injectable.memoryFix:
return await loadBinary("leakv2.dll", true);
return File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
}
}
@@ -372,16 +381,11 @@ class _LaunchButtonState extends State<LaunchButton> {
return dllPath;
}
await _downloadMissingDll(injectable);
if(dllPath.existsSync()) {
return dllPath;
}
_onDllFail(dllPath, type);
_onDllFail(dllPath, hosting);
return null;
}
void _onDllFail(File dllPath, GameType type) {
void _onDllFail(File dllPath, bool hosting) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(_fail){
return;
@@ -390,23 +394,14 @@ class _LaunchButtonState extends State<LaunchButton> {
_fail = true;
_closeDialogIfOpen(false);
showMissingDllError(path.basename(dllPath.path));
_onStop(type);
_onStop(hosting);
});
}
Future<void> _downloadMissingDll(Injectable injectable) async {
if(injectable != Injectable.reboot){
await loadBinary("$injectable.dll", true);
return;
}
await downloadRebootDll(rebootDownloadUrl, 0);
}
}
enum Injectable {
console,
cranium,
sslBypass,
reboot,
memoryFix
}

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
class VersionNameInput extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();

View File

@@ -4,15 +4,15 @@ 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/dialog.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/dialog.dart';
import 'package:reboot_launcher/src/ui/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';
import 'package:reboot_launcher/src/ui/dialog/add_local_version.dart';
import 'package:reboot_launcher/src/ui/widget/shared/smart_check_box.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:reboot_launcher/src/dialog/add_server_version.dart';
import 'package:reboot_launcher/src/ui/dialog/add_server_version.dart';
import 'package:reboot_launcher/src/util/checks.dart';
import '../shared/file_selector.dart';
@@ -60,10 +60,9 @@ class _VersionSelectorState extends State<VersionSelector> {
.toList();
MenuFlyoutItem _createDefaultVersionItem() => MenuFlyoutItem(
text: const SizedBox(
width: double.infinity, child: Text("No versions available. Add it using the buttons on the right.")),
trailing: const Expanded(child: SizedBox()),
onPressed: () {});
text: const Text("Please create or download a version"),
onPressed: () {}
);
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
text: _createOptionsMenu(

View File

@@ -1,7 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
import 'package:reboot_launcher/src/ui/dialog/server_dialogs.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
class ServerButton extends StatefulWidget {
@@ -26,7 +26,7 @@ class _ServerButtonState extends State<ServerButton> {
alignment: Alignment.center,
child: Text(_buttonText),
),
onPressed: () => _serverController.toggle()
onPressed: () => _serverController.toggle(false)
),
)),
),

View File

@@ -1,6 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/ui/controller/server_controller.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
class ServerTypeSelector extends StatelessWidget {

View File

@@ -4,7 +4,7 @@ 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 'package:reboot_launcher/src/ui/dialog/snackbar.dart';
import 'package:reboot_launcher/src/util/selector.dart';
@@ -16,6 +16,7 @@ class FileSelector extends StatefulWidget {
final String? Function(String?) validator;
final AutovalidateMode? validatorMode;
final String? extension;
final String? label;
final bool folder;
const FileSelector(
@@ -24,6 +25,7 @@ class FileSelector extends StatefulWidget {
required this.controller,
required this.validator,
required this.folder,
this.label,
this.extension,
this.validatorMode,
this.allowNavigator = true,
@@ -40,30 +42,35 @@ class _FileSelectorState extends State<FileSelector> {
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormBox(
return widget.label != null ? InfoLabel(
label: widget.label!,
child: _buildBody,
) : _buildBody;
}
Widget get _buildBody => Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormBox(
controller: widget.controller,
placeholder: widget.placeholder,
validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
)
),
if (widget.allowNavigator)
const SizedBox(width: 16.0),
if (widget.allowNavigator)
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
)
)
],
);
}
),
if (widget.allowNavigator)
const SizedBox(width: 16.0),
if (widget.allowNavigator)
Padding(
padding: const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
)
)
],
);
void _onPressed() {
if(_selecting){

View File

@@ -0,0 +1,90 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/ui/widget/shared/fluent_card.dart';
class SettingTile extends StatefulWidget {
static const double kDefaultContentWidth = 200.0;
static const double kDefaultSpacing = 8.0;
final String title;
final String subtitle;
final Widget? content;
final double? contentWidth;
final List<Widget>? expandedContent;
final double expandedContentSpacing;
final bool isChild;
const SettingTile(
{Key? key,
required this.title,
required this.subtitle,
this.content,
this.contentWidth = kDefaultContentWidth,
this.expandedContentSpacing = kDefaultSpacing,
this.expandedContent,
this.isChild = false})
: super(key: key);
@override
State<SettingTile> createState() => _SettingTileState();
}
class _SettingTileState extends State<SettingTile> {
@override
Widget build(BuildContext context) {
if(widget.expandedContent == null){
return _contentCard;
}
return Mica(
elevation: 1,
child: Expander(
initiallyExpanded: true,
contentBackgroundColor: FluentTheme.of(context).menuColor,
headerShape: (open) => const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
),
header: _header,
headerHeight: 72,
trailing: _trailing,
content: _content
),
);
}
Widget get _content {
var contents = widget.expandedContent!;
var items = List.generate(contents.length * 2, (index) => index % 2 == 0 ? contents[index ~/ 2] : SizedBox(height: widget.expandedContentSpacing));
return Column(
children: items
);
}
Widget get _trailing => SizedBox(
width: widget.contentWidth,
child: widget.content
);
Widget get _header => ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle)
);
Widget get _contentCard {
if (widget.isChild) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: _contentCardBody
);
}
return FluentCard(
child: _contentCardBody,
);
}
Widget get _contentCardBody => ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle),
trailing: _trailing
);
}

View File

@@ -1,16 +1,18 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
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:path/path.dart' as path;
import 'package:unrar_file/unrar_file.dart';
import 'os.dart';
final _manifestSourceUrl = Uri.parse(
"https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md");
final Uri _manifestSourceUrl = Uri.parse(
"https://github.com/simplyblk/Fortnitebuilds/blob/main/README.md");
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
var response = await http.get(_manifestSourceUrl);
@@ -19,45 +21,65 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
}
var document = parse(response.body);
var table = document.querySelector("table");
if (table == null) {
throw Exception("Missing data table");
}
var elements = document.getElementsByTagName("table")
.map((element) => element.querySelector("tbody"))
.expand((element) => element?.getElementsByTagName("tr") ?? [])
.toList();
var results = <FortniteBuild>[];
for (var tableEntry in table.querySelectorAll("tbody > tr")) {
for (var tableEntry in elements) {
var children = tableEntry.querySelectorAll("td");
var name = children[0].text;
var minifiedName = name.substring(name.indexOf("-") + 1, name.lastIndexOf("-"));
var version = parser
.tryParse(minifiedName.replaceFirst("", ""));
var version = parser.tryParse(children[0].text);
if (version == null) {
continue;
}
var link = children[2].firstChild!.attributes["href"]!;
var link = children[3].firstChild?.attributes?["href"];
if (link == null || link == "N/A") {
continue;
}
results.add(FortniteBuild(version: version, link: link));
}
return results;
}
Future<Process> downloadManifestBuild(
String manifestUrl, String destination, Function(double, String) onProgress) async {
var log = await loadBinary("download.txt", true);
await log.create();
Future<void> downloadArchiveBuild(String archiveUrl, Directory destination, Function(double, String) onProgress, Function() onRar) async {
var outputDir = await destination.createTemp("build");
try {
destination.createSync(recursive: true);
var fileName = archiveUrl.substring(archiveUrl.lastIndexOf("/") + 1);
var extension = path.extension(fileName);
var tempFile = File("${outputDir.path}//$fileName");
var startTime = DateTime.now().millisecondsSinceEpoch;
var client = http.Client();
var response = await client.send(
http.Request("GET", Uri.parse(archiveUrl)));
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
}
var buildExe = await loadBinary("build.exe", true);
var process = await Process.start(buildExe.path, [manifestUrl, destination]);
log.writeAsString("Starting download of: $manifestUrl\n", mode: FileMode.append);
process.errLines
.where((message) => message.contains("%"))
.forEach((message) {
log.writeAsString("$message\n", mode: FileMode.append);
onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1));
});
return process;
}
var length = response.contentLength!;
var received = 0;
var sink = tempFile.openWrite();
await response.stream.map((s) {
received += s.length;
var now = DateTime.now();
var eta = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch;
onProgress((received / length) * 100, toETA(eta));
return s;
}).pipe(sink);
onRar();
if(extension.toLowerCase() == ".zip"){
await extractFileToDisk(tempFile.path, destination.path);
}else if(extension.toLowerCase() == ".rar") {
await UnrarFile.extract_rar(tempFile.path, destination.path);
} else {
throw Exception("Unknown file extension: $extension");
}
} catch(message) {
throw Exception("Cannot download build: $message");
}finally {
outputDir.delete(recursive: true);
}
}

View File

@@ -1,8 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import '../../main.dart';
import '../page/home_page.dart';
import '../dialog/dialog.dart';
import '../../../main.dart';
import '../ui/dialog/dialog.dart';
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
if(exception == null){
@@ -13,7 +12,7 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
return;
}
showDialog(
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showDialog(
context: appKey.currentContext!,
builder: (context) =>
ErrorDialog(
@@ -21,5 +20,5 @@ void onError(Object? exception, StackTrace? stackTrace, bool framework) {
stackTrace: stackTrace,
errorMessageBuilder: (exception) => framework ? "An error was thrown by Flutter: $exception" : "An uncaught error was thrown: $exception"
)
);
));
}

View File

@@ -19,32 +19,6 @@ bool get isWin11 {
return intBuild != null && intBuild > 22000;
}
Future<File> loadBinary(String binary, bool safe) async{
var safeBinary = File("${safeBinariesDirectory.path}\\$binary");
if(await safeBinary.exists()){
return safeBinary;
}
var internal = _locateInternalBinary(binary);
if(!safe){
return internal;
}
if(await internal.exists()){
await internal.copy(safeBinary.path);
}
return safeBinary;
}
File _locateInternalBinary(String binary) =>
File("${internalAssetsDirectory.path}\\binaries\\$binary");
Future<void> resetWinNat() async {
var binary = await loadBinary("winnat.bat", true);
await runElevated(binary.path, "");
}
Future<bool> runElevated(String executable, String args) async {
var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();
@@ -57,57 +31,30 @@ Future<bool> runElevated(String executable, String args) async {
return shellResult == 1;
}
Directory get internalAssetsDirectory =>
Directory("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets");
Directory get installationDirectory =>
File(Platform.resolvedExecutable).parent;
Directory get logsDirectory =>
Directory("${installationDirectory.path}\\logs");
Directory get assetsDirectory =>
Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
Directory get tempDirectory =>
Directory("${Platform.environment["Temp"]}");
Directory(Platform.environment["Temp"]!);
Directory get safeBinariesDirectory =>
Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher");
Directory get embeddedBackendDirectory =>
Directory("${safeBinariesDirectory.path}\\backend-lawin");
File loadEmbedded(String file) {
var safeBinary = File("${embeddedBackendDirectory.path}\\$file");
if(safeBinary.existsSync()){
return safeBinary;
Future<bool> delete(FileSystemEntity file) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return Future.delayed(const Duration(seconds: 5)).then((value) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return false;
}
});
}
safeBinary.parent.createSync(recursive: true);
var internal = File("${internalAssetsDirectory.path}\\$file");
if(internal.existsSync()) {
internal.copySync(safeBinary.path);
}
return safeBinary;
}
Directory loadEmbeddedDirectory(String directory) {
var safeBinary = Directory("${embeddedBackendDirectory.path}\\$directory");
safeBinary.parent.createSync(recursive: true);
var internal = Directory("${internalAssetsDirectory.path}\\$directory");
_copyFolder(internal, safeBinary);
return safeBinary;
}
void _copyFolder(Directory dir1, Directory dir2) {
if(!dir1.existsSync()){
return;
}
if (!dir2.existsSync()) {
dir2.createSync(recursive: true);
}
dir1.listSync().forEach((element) {
var newPath = "${dir2.path}/${path.basename(element.path)}";
if (element is File) {
var newFile = File(newPath);
newFile.writeAsBytesSync(element.readAsBytesSync());
} else if (element is Directory) {
_copyFolder(element, Directory(newPath));
}
});
}

View File

@@ -4,53 +4,40 @@ import 'package:archive/archive_io.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/model/reboot_download.dart';
import 'package:reboot_launcher/src/util/os.dart';
const String rebootDownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip";
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/main/Release.zip";
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
Future<RebootDownload> downloadRebootDll(String url, int? lastUpdateMs) async {
Directory? outputDir;
File? tempZip;
try {
var now = DateTime.now();
var oldRebootDll = await loadBinary("reboot.dll", true);
var lastUpdate = await _getLastUpdate(lastUpdateMs);
var exists = await oldRebootDll.exists();
if (lastUpdate != null &&
now.difference(lastUpdate).inHours <= 24 &&
await oldRebootDll.exists()) {
return RebootDownload(lastUpdateMs!);
}
var response = await http.get(Uri.parse(rebootDownloadUrl));
var tempZip = await loadBinary("reboot.zip", true);
await tempZip.writeAsBytes(response.bodyBytes);
var outputDir = await safeBinariesDirectory.createTemp("reboot_out");
await extractFileToDisk(tempZip.path, outputDir.path);
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())) {
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
}
return RebootDownload(now.millisecondsSinceEpoch);
} catch (error, stackTrace) {
return RebootDownload(-1, error, stackTrace);
} finally {
Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
Directory? outputDir;
try {
outputDir?.delete(recursive: true);
tempZip?.delete();
} catch (_) {}
}
var now = DateTime.now();
var lastUpdate = await _getLastUpdate(lastUpdateMs);
var exists = await rebootDllFile.exists();
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists) {
return lastUpdateMs!;
}
var response = await http.get(Uri.parse(rebootDownloadUrl));
outputDir = await installationDirectory.createTemp("reboot_out");
var tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, outputDir.path);
var rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
if (!exists || sha1.convert(await rebootDllFile.readAsBytes()) != sha1.convert(await rebootDll.readAsBytes())) {
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes());
}
return now.millisecondsSinceEpoch;
}catch(message) {
throw Exception("Cannot download reboot.zip, invalid zip: $message");
}finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {

View File

@@ -1,21 +1,23 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:archive/archive_io.dart';
import 'package:ini/ini.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/ui/controller/game_controller.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:shelf/shelf_io.dart';
import 'package:http/http.dart' as http;
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
final serverLogFile = File("${logsDirectory.path}\\server.log");
final serverDirectory = Directory("${assetsDirectory.path}\\lawin");
final serverExeFile = File("${serverDirectory.path}\\lawinserver-win.exe");
Future<void> writeMatchmakingIp(String text) async {
var file = File("${embeddedBackendDirectory.path}\\Config\\config.ini");
var file = File("${assetsDirectory.path}\\lawin\\Config\\config.ini");
if(!file.existsSync()){
return;
}
@@ -29,24 +31,25 @@ Future<void> writeMatchmakingIp(String text) async {
file.writeAsStringSync(config.toString());
}
Future<void> startServer() async {
if(!embeddedBackendDirectory.existsSync()){
var serverZip = await loadBinary("server.zip", true);
await extractFileToDisk(serverZip.path, embeddedBackendDirectory.path);
}
Future<void> startServer(bool detached) async {
var process = await Process.start(
"${embeddedBackendDirectory.path}\\lawinserver-win.exe",
serverExeFile.path,
[],
workingDirectory: embeddedBackendDirectory.path
workingDirectory: serverDirectory.path,
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal
);
process.outLines.forEach((element) => serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
process.errLines.forEach((element) => serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
if(!detached) {
serverLogFile.createSync(recursive: true);
process.outLines.forEach((element) =>
serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
process.errLines.forEach((element) =>
serverLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
}
}
Future<void> stopServer() async {
var releaseBat = await loadBinary("kill_both_ports.bat", false);
await Process.run(releaseBat.path, []);
await freeLawinPort();
await freeMatchmakerPort();
}
Future<bool> isLawinPortFree() async {
@@ -64,24 +67,23 @@ Future<bool> isMatchmakerPortFree() async {
}
Future<void> freeLawinPort() async {
var releaseBat = await loadBinary("kill_lawin_port.bat", false);
var result = await Process.run(releaseBat.path, []);
if(result.exitCode == 1){
await runElevated(releaseBat.path, "");
await Future.delayed(const Duration(seconds: 1));
}
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_lawin.bat");
await Process.run(releaseBat.path, []);
}
Future<void> freeMatchmakerPort() async {
var releaseBat = await loadBinary("kill_matchmaker_port.bat", false);
var result = await Process.run(releaseBat.path, []);
if(result.exitCode == 1){
await runElevated(releaseBat.path, "");
await Future.delayed(const Duration(seconds: 1));
}
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_matchmaker.bat");
await Process.run(releaseBat.path, []);
}
List<String> createRebootArgs(String username, GameType type, String additionalArgs) {
Future<void> resetWinNat() async {
var binary = File("${serverDirectory.path}\\winnat.bat");
await runElevated(binary.path, "");
}
List<String> createRebootArgs(String username, bool host, String additionalArgs) {
username = username.isEmpty ? kDefaultPlayerName : username;
username = host ? "$username${Random().nextInt(1000)}" : username;
var args = [
"-epicapp=Fortnite",
"-epicenv=Prod",
@@ -91,18 +93,13 @@ List<String> createRebootArgs(String username, GameType type, String additionalA
"-nobe",
"-fromfl=eac",
"-fltoken=3db3ba5dcbd2e16703f3978d",
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ"
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN=$username@projectreboot.dev",
"-AUTH_PASSWORD=Rebooted",
"-AUTH_TYPE=epic"
];
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){
if(host){
args.addAll([
"-nullrhi",
"-nosplash",
@@ -205,5 +202,5 @@ enum ServerResultType {
canStart,
alreadyStarted,
unknownError,
stopped,
stopped
}

View File

@@ -1,5 +1,6 @@
String toETA(int milliseconds){
var duration = Duration(milliseconds: milliseconds);
String toETA(double milliseconds){
var roundedMilliseconds = milliseconds.toInt();
var duration = Duration(milliseconds: roundedMilliseconds);
return "${duration.inHours.toString().padLeft(2, "0")}:"
"${duration.inMinutes.remainder(60).toString().padLeft(2, "0")}:"
"${duration.inSeconds.remainder(60).toString().padLeft(2, "0")}";

View File

@@ -1,30 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
class GameTypeSelector extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
GameTypeSelector({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() => DropDownButton(
leading: Text(_gameController.type.value.name),
items: GameType.values
.map((type) => _createItem(type))
.toList()
));
}
MenuFlyoutItem _createItem(GameType type) => MenuFlyoutItem(
text: Text(type.name),
onPressed: () {
_gameController.type(type);
_gameController.started.value = _gameController.currentGameInstance != null;
}
);
}

View File

@@ -1,24 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
class HostInput extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
HostInput({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Tooltip(
message: "The hostname of the backend server",
child: Obx(() => SmartInput(
label: "Host",
placeholder: "Type the backend server's hostname",
controller: _serverController.host,
enabled: _serverController.type.value == ServerType.remote
))
);
}
}

View File

@@ -1,25 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/shared/smart_input.dart';
class PortInput extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
PortInput({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Tooltip(
message: "The port of the backend server",
child: Obx(() => SmartInput(
label: "Port",
placeholder: "Type the backend server's port",
controller: _serverController.port,
enabled: _serverController.type.value == ServerType.remote
))
);
}
}

View File

@@ -1,64 +0,0 @@
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/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/util/selector.dart';
class RebootUpdaterInput extends StatefulWidget {
const RebootUpdaterInput({Key? key}) : super(key: key);
@override
State<RebootUpdaterInput> createState() => _RebootUpdaterInputState();
}
class _RebootUpdaterInputState extends State<RebootUpdaterInput> {
final SettingsController _settingsController = Get.find<SettingsController>();
final RxBool _valid = RxBool(true);
late String? Function(String?) validator;
@override
void initState() {
validator = (value) {
var result = value != null && Uri.tryParse(value) != null;
WidgetsBinding.instance.addPostFrameCallback((_) => _valid.value = result);
return result ? null : "Invalid URL";
};
super.initState();
}
@override
Widget build(BuildContext context) {
return InfoLabel(
label: "Reboot Updater",
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Obx(() => Expanded(
child: TextFormBox(
controller: _settingsController.updateUrl,
placeholder: "Type the URL of the reboot updater",
validator: validator,
autovalidateMode: AutovalidateMode.always,
enabled: _settingsController.autoUpdate.value
)
)),
const SizedBox(width: 16.0),
Tooltip(
message: _settingsController.autoUpdate.value ? "Disable automatic updates" : "Enable automatic updates",
child: Obx(() => Button(
onPressed: () => _settingsController.autoUpdate.value = !_settingsController.autoUpdate.value,
child: Icon(_settingsController.autoUpdate.value ? FluentIcons.disable_updates : FluentIcons.refresh)
)
)
)
],
)
);
}
}

View File

@@ -1,67 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/widget/shared/fluent_card.dart';
class SettingTile extends StatefulWidget {
static const double kDefaultContentWidth = 200.0;
final String title;
final String subtitle;
final Widget? content;
final double? contentWidth;
final List<Widget>? expandedContent;
const SettingTile(
{Key? key,
required this.title,
required this.subtitle,
this.content,
this.contentWidth = kDefaultContentWidth,
this.expandedContent})
: super(key: key);
@override
State<SettingTile> createState() => _SettingTileState();
}
class _SettingTileState extends State<SettingTile> {
@override
Widget build(BuildContext context) {
if(widget.expandedContent == null){
return _contentCard;
}
return Mica(
elevation: 1,
child: Expander(
initiallyExpanded: true,
contentBackgroundColor: FluentTheme.of(context).menuColor,
headerShape: (open) => const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(4.0)),
),
header: ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle)
),
headerHeight: 72,
trailing: SizedBox(
width: widget.contentWidth,
child: widget.content
),
content: Column(
children: widget.expandedContent!
)
),
);
}
Widget get _contentCard => FluentCard(
child: ListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle),
trailing: SizedBox(
width: widget.contentWidth,
child: widget.content
),
),
);
}

View File

@@ -1,50 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:system_theme/system_theme.dart';
class SmartSwitch extends StatefulWidget {
final String? label;
final bool enabled;
final Function()? onDisabledPress;
final Rx<bool> value;
const SmartSwitch(
{Key? key,
required this.value,
this.label,
this.enabled = true,
this.onDisabledPress})
: super(key: key);
@override
State<SmartSwitch> createState() => _SmartSwitchState();
}
class _SmartSwitchState extends State<SmartSwitch> {
@override
Widget build(BuildContext context) {
return widget.label == null ? _createSwitch() : _createLabel();
}
InfoLabel _createLabel() {
return InfoLabel(
label: widget.label!,
child: _createSwitch()
);
}
Widget _createSwitch() {
return Obx(() => ToggleSwitch(
checked: widget.value.value,
onChanged: _onChanged
)
);
}
void _onChanged(bool checked) {
if (!widget.enabled) {
return;
}
setState(() => widget.value.value = checked);
}
}