10 Commits

Author SHA1 Message Date
Alessandro Autiero
62dae468bf Merge pull request #98 from Auties00/_onLoggedIn
Released 9.2.6
2024-09-12 17:49:12 +02:00
Alessandro Autiero
232bf8fbfc Update README.md 2024-08-18 22:35:17 +02:00
Alessandro Autiero
a787c4efc9 Merge pull request #86 from Auties00/_onLoggedIn
Release 9.2.5
2024-08-18 22:34:36 +02:00
Alessandro Autiero
3f88d5ed80 Create .gitattributes 2024-07-31 11:54:02 +02:00
Alessandro Autiero
170a878e79 Merge pull request #69 from Auties00/_onLoggedIn
Release 9.2.2
2024-07-09 22:38:47 +02:00
Alessandro Autiero
00802ac6da Merge pull request #65 from Auties00/_onLoggedIn
Release 9.2.0
2024-07-06 20:44:33 +02:00
Alessandro Autiero
1d68469297 Merge pull request #64 from Auties00/_onLoggedIn
Port Forwarding documentation
2024-07-06 20:35:08 +02:00
Alessandro Autiero
83878db8c5 Merge pull request #63 from Auties00/_onLoggedIn
Portforwarding documentation
2024-07-06 20:33:57 +02:00
Alessandro Autiero
018ccb7f8e Merge pull request #57 from Auties00/_onLoggedIn
9.1.4
2024-06-15 19:57:54 +02:00
Alessandro Autiero
0a775e2f3f Merge pull request #52 from Auties00/_onLoggedIn
9.1.3
2024-06-04 22:32:10 +02:00
69 changed files with 921 additions and 3134 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
backend/**/* linguist-vendored

View File

@@ -8,7 +8,9 @@ Join our discord at https://discord.gg/reboot
- COMMON: Shared business logic for CLI and GUI modules
- CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart
- GUI: Stable graphical user interface to play and host Fortnite S0-14
![image](https://github.com/user-attachments/assets/7ff5d49e-8920-41ad-a805-188d84ad6ec4)
## Installation
Check the releases section
Check the releases section

View File

@@ -1,5 +1,4 @@
const String kDefaultPlayerName = "Player";
const String kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777";
const String kInitializedLine = "Game Engine Initialized";

View File

@@ -1,9 +1,6 @@
enum InjectableDll {
console,
starfall,
cobalt,
reboot,
}
extension InjectableDllVersionAware on InjectableDll {
bool get isVersionDependent => this == InjectableDll.reboot;
memory
}

View File

@@ -17,15 +17,13 @@ class FortniteBuild {
class FortniteBuildDownloadProgress {
final double progress;
final int? timeLeft;
final int? minutesLeft;
final bool extracting;
final int speed;
FortniteBuildDownloadProgress({
required this.progress,
required this.extracting,
required this.timeLeft,
required this.speed
this.minutesLeft,
});
}

View File

@@ -1,11 +1,10 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:version/version.dart';
class GameInstance {
final Version version;
final String versionName;
final int gamePid;
final int? launcherPid;
final int? eacPid;
@@ -18,7 +17,7 @@ class GameInstance {
GameInstance? child;
GameInstance({
required this.version,
required this.versionName,
required this.gamePid,
required this.launcherPid,
required this.eacPid,

View File

@@ -3,243 +3,165 @@ import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart';
import 'package:uuid/uuid.dart';
import 'package:version/version.dart';
import 'package:http/http.dart' as http;
const String kStopBuildDownloadSignal = "kill";
final Uri _archiveSourceUrl = Uri.parse("https://builds.rebootfn.org/versions.json");
final int _ariaPort = 6800;
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
final String _ariaSecret = "RebootLauncher";
final Dio _dio = _buildDioInstance();
Dio _buildDioInstance() {
final dio = Dio();
final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter;
httpClientAdapter.createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
return client;
};
return dio;
}
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
const String _unavailableError = "The build downloader is not available right now";
const String _genericError = "The build downloader is not working correctly";
const int _maxErrors = 100;
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
final response = await http.get(_archiveSourceUrl);
final response = await _dio.get<String>(
_archiveSourceUrl,
options: Options(
responseType: ResponseType.plain
)
);
if (response.statusCode != 200) {
return [];
}
return jsonDecode(response.body)
.map((entry) {
try {
final fileUrl = entry as String;
final fileName = Uri.parse(fileUrl).pathSegments.last;
final fileNameWithoutExtension = path.basenameWithoutExtension(fileName);
return FortniteBuild(
version: Version.parse(fileNameWithoutExtension),
link: entry,
available: true
);
}catch(_) {
return null;
}
})
.whereType<FortniteBuild>()
.toList();
var results = <FortniteBuild>[];
for (final line in response.data?.split("\n") ?? []) {
if (!line.startsWith("|")) {
continue;
}
var parts = line.substring(1, line.length - 1).split("|");
if (parts.isEmpty) {
continue;
}
var versionName = parts.first.trim();
final separator = versionName.indexOf("-");
if(separator != -1) {
versionName = versionName.substring(0, separator);
}
final link = parts.last.trim();
try {
results.add(FortniteBuild(
version: Version.parse(versionName),
link: link,
available: link.endsWith(".zip") || link.endsWith(".rar")
));
} on FormatException {
// Ignore
}
}
return results;
}
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
try {
final stopped = _setupLifecycle(options);
await outputFile.parent.create(recursive: true);
final downloadItemCompleter = Completer<File>();
await _startAriaServer();
final downloadId = await _startAriaDownload(options, outputFile);
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.tellStatus",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
}
final result = statusResponseJson["result"];
final files = result["files"] as List?;
if(files == null || files.isEmpty) {
downloadItemCompleter.completeError("Download aborted");
timer.cancel();
return;
}
final error = result["errorCode"];
if(error != null) {
final errorCode = int.tryParse(error);
if(errorCode == 0) {
final path = File(files[0]["path"]);
downloadItemCompleter.complete(path);
}else if(errorCode == 3) {
downloadItemCompleter.completeError("This build is not available yet");
}else {
final errorMessage = result["errorMessage"];
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
}
timer.cancel();
return;
}
final speed = int.parse(result["downloadSpeed"] ?? "0");
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
final totalLength = int.parse(files[0]["length"] ?? "0");
final percentage = completedLength * 100 / totalLength;
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
_onProgress(
options.port,
percentage,
speed,
minutesLeft,
false
);
}catch(error) {
throw "Invalid download status (${error})";
}
});
await Future.any([stopped.future, downloadItemCompleter.future]);
if(!stopped.isCompleted) {
final extension = path.extension(fileName);
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
}else {
await _stopAriaDownload(downloadId);
final outputDir = Directory("${options.destination.path}\\.build");
await outputDir.create(recursive: true);
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final extension = path.extension(fileName);
final tempFile = File("${outputDir.path}\\$fileName");
if(await tempFile.exists()) {
await tempFile.delete(recursive: true);
}
final startTime = DateTime.now().millisecondsSinceEpoch;
final response = _downloadArchive(options, stopped, tempFile, startTime);
await Future.any([stopped.future, response]);
if(!stopped.isCompleted) {
await _extractArchive(stopped, extension, tempFile, options);
}
delete(outputDir);
}catch(error) {
_onError(error, options);
}finally {
delete(outputFile);
}
}
Future<void> _startAriaServer() async {
final running = await _isAriaRunning();
if(running) {
await killProcessByPort(_ariaPort);
}
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, Completer stopped, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
var received = byteStart ?? 0;
try {
await _dio.download(
options.build.link,
tempFile.path,
onReceiveProgress: (data, length) {
if(stopped.isCompleted) {
throw StateError("Download interrupted");
}
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe";
}
received = data;
final percentage = (received / length) * 100;
_onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options);
},
deleteOnError: false,
options: Options(
validateStatus: (statusCode) {
if(statusCode == 200) {
return true;
}
await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-secret=$_ariaSecret",
"--rpc-listen-port=$_ariaPort"
],
window: false
);
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) {
if(statusCode == 403 || statusCode == 503) {
throw _deniedConnectionError;
}
if(statusCode == 404) {
throw _unavailableError;
}
throw _genericError;
},
headers: byteStart == null || byteStart <= 0 ? {
"Cookie": "_c_t_c=1"
} : {
"Cookie": "_c_t_c=1",
"Range": "bytes=${byteStart}-"
},
)
);
}catch(error) {
if(stopped.isCompleted) {
return;
}
await Future.delayed(const Duration(seconds: 1));
}
throw "cannot start download server (timeout exceeded)";
}
Future<bool> _isAriaRunning() async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.getVersion",
"params": [
"token:${_ariaSecret}"
]
};
await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return true;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
"token:${_ariaSecret}",
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) {
_onError(error, options);
return;
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
await _downloadArchive(options, stopped, tempFile, startTime, received, errorsCount + 1);
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
final startTime = DateTime.now().millisecondsSinceEpoch;
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
throw "Missing 7zip.exe";
throw "Corrupted installation: missing 7zip.exe";
}
process = await startProcess(
@@ -254,15 +176,10 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
);
var completed = false;
process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch;
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
_onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt);
return;
}
@@ -273,13 +190,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
}
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
_onProgress(startTime, now, percentage, true, options);
});
process.stdError.listen((data) {
if(!data.isBlank) {
@@ -295,7 +206,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
throw "Missing winrar.exe";
throw "Corrupted installation: missing winrar.exe";
}
process = await startProcess(
@@ -310,16 +221,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
);
var completed = false;
process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch;
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
_onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt);
return;
}
@@ -330,13 +236,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
}
final percentage = int.parse(element).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
_onProgress(startTime, now, percentage, true, options);
});
process.stdError.listen((data) {
if(!data.isBlank) {
@@ -357,22 +257,21 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
process.kill(ProcessSignal.sigabrt);
}
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
if(percentage == 0) {
port.send(FortniteBuildDownloadProgress(
options.port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: null,
speed: speed
extracting: extracting
));
return;
}
port.send(FortniteBuildDownloadProgress(
final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now;
final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round();
options.port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: minutesLeft,
speed: speed
minutesLeft: minutesLeft
));
}
@@ -392,5 +291,4 @@ Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}
}

View File

@@ -6,16 +6,13 @@ import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
bool _watcher = false;
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
const String kRebootBelowS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
const String kRebootAboveS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
const String kRebootDownloadUrl =
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
final exists = await rebootDllFile.exists();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
@@ -31,8 +28,9 @@ Future<void> downloadCriticalDll(String name, String outputPath) async {
await output.writeAsBytes(response.bodyBytes, flush: true);
}
Future<void> downloadRebootDll(File file, String url) async {
Future<int> downloadRebootDll(String url) async {
Directory? outputDir;
final now = DateTime.now();
try {
final response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
@@ -44,7 +42,8 @@ Future<void> downloadRebootDll(File file, String url) async {
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
return now.millisecondsSinceEpoch;
} finally{
if(outputDir != null) {
delete(outputDir);
@@ -64,7 +63,7 @@ Stream<String> watchDlls() async* {
}
_watcher = true;
await for(final event in dllsDirectory.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (event.path.endsWith(".dll")) {
yield event.path;
}

View File

@@ -8,7 +8,10 @@ import 'dart:isolate';
import 'dart:math';
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/util/log.dart';
import 'package:sync/semaphore.dart';
import 'package:win32/win32.dart';
final _ntdll = DynamicLibrary.open('ntdll.dll');
@@ -95,8 +98,8 @@ Future<bool> startElevatedProcess({required String executable, required String a
var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
return ShellExecuteEx(shellInput) == 1;
@@ -151,36 +154,47 @@ final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess');
bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtSuspendProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = _NtSuspendProcess(processHandle);
CloseHandle(processHandle);
return result == 0;
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtResumeProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = _NtResumeProcess(processHandle);
CloseHandle(processHandle);
return result == 0;
}
Future<void> watchProcess(int pid) => Isolate.run(() {
final processHandle = OpenProcess(FILE_ACCESS_RIGHTS.SYNCHRONIZE, FALSE, pid);
if (processHandle == 0) {
return;
}
void _watchProcess(int pid) {
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
try {
WaitForSingleObject(processHandle, INFINITE);
}finally {
CloseHandle(processHandle);
}
});
}
Future<bool> watchProcess(int pid) async {
var completer = Completer<bool>();
var exitPort = ReceivePort();
exitPort.listen((_) {
if(!completer.isCompleted) {
completer.complete(true);
}
});
var errorPort = ReceivePort();
errorPort.listen((_) => completer.complete(false));
await Isolate.spawn(
_watchProcess,
pid,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
errorsAreFatal: true
);
return await completer.future;
}
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
log("[PROCESS] Generating reboot args");

View File

@@ -7,18 +7,19 @@ environment:
sdk: ">=3.0.0 <=4.0.0"
dependencies:
win32: ^5.5.4
ffi: ^2.1.3
path: ^1.9.0
http: ^1.2.2
crypto: ^3.0.5
archive: ^3.6.1
dio: ^5.3.2
win32: 3.0.0
ffi: ^2.1.0
path: ^1.8.3
http: ^1.1.0
crypto: ^3.0.2
archive: ^3.3.7
ini: ^2.1.0
shelf_proxy: ^1.0.2
sync: ^0.3.0
uuid: ^4.5.1
uuid: ^3.0.6
shelf_web_socket: ^2.0.0
version: ^3.0.2
dev_dependencies:
flutter_lints: ^5.0.0
flutter_lints: ^2.0.1

View File

@@ -1,39 +1,16 @@
# reboot_launcher
# Reboot Launcher
Welcome to the **Reboot Launcher**!
This is a GUI application developed as part of the **Reboot Project**.
Launcher for project reboot
## Getting Started
### Running the Project
To launch the project in development mode, simply run:
```
flutter run
```
This project is a starting point for a Flutter application.
### Building the Project
To create a production-ready build, use:
```
flutter build
```
A few resources to get you started if this is your first Flutter project:
### Packaging the Project
To package the application for distribution, run:
```
package.bat
```
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
## Requirements
- [Flutter SDK](https://flutter.dev/docs/get-started/install)
- Supported operating systems: Windows
## Other platforms
Native support for these platforms is not currently planned, but Linux support is a priority for the 10.0 release cycle
- [Linux Tutorial using Proton](https://www.reddit.com/r/linux_gaming/comments/1fwa4l8/guide_running_a_fortnite_private_server_to_play/)
- No tutorials are available for MacOS(got lost when the Reboot discord was banned), but it's possible to run Reboot using a compatibility layer
## Contributing
Contributions are welcome! Feel free to open an issue or submit a pull request.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
taskkill /f /im winrar.exe
taskkill /f /im tar.exe

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -76,7 +76,7 @@
"playGameServerCustomContent": "Enter IP",
"settingsName": "Settings",
"settingsClientName": "Internal files",
"settingsClientDescription": "Configure the internal files used by the launcher",
"settingsClientDescription": "Configure the internal files used by the launcher for Fortnite",
"settingsClientOptionsName": "Options",
"settingsClientOptionsDescription": "Configure additional options for Fortnite",
"settingsClientConsoleName": "Unreal engine patcher",
@@ -94,19 +94,18 @@
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
"settingsServerOptionsName": "Options",
"settingsServerOptionsSubtitle": "Configure additional options for the game server",
"settingsServerTypeName": "Game server type",
"settingsServerTypeName": "Type",
"settingsServerTypeDescription": "The type of game server to inject",
"settingsServerTypeEmbeddedName": "Embedded",
"settingsServerTypeCustomName": "Custom",
"settingsOldServerFileName": "Game server",
"settingsServerFileName": "Implementation",
"settingsServerFileDescription": "The file injected to create the game server",
"settingsServerPortName": "Port",
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
"settingsServerOldMirrorName": "Update mirror (Before season 20)",
"settingsServerNewMirrorName": "Update mirror (Season 20 and above)",
"settingsServerMirrorName": "Update mirror",
"settingsServerMirrorDescription": "The URL used to update the game server dll",
"settingsServerMirrorPlaceholder": "mirror",
"settingsServerTimerName": "Game server updater",
"settingsServerTimerName": "Update timer",
"settingsServerTimerSubtitle": "Determines when the game server should be updated",
"settingsUtilsName": "Launcher",
"settingsUtilsSubtitle": "This section contains settings related to the launcher",
@@ -216,8 +215,6 @@
"downloadedVersion": "The download was completed successfully!",
"download": "Download",
"downloading": "Downloading...",
"allocatingSpace": "Allocating disk space...",
"startingDownload": "Starting download...",
"extracting": "Extracting...",
"buildProgress": "{progress}%",
"buildInstallationDirectory": "Installation directory",
@@ -327,8 +324,6 @@
"backendErrorMessage": "The backend reported an unexpected error",
"welcomeTitle": "Welcome to Reboot Launcher",
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
"hostAccountText": "The host tab shows different credentials compared to the play tab.\nIf you are advanced user, you can set a different email and password\nhere if the backend you are using needs authentication.",
"hostAccountAction": "I understand",
"welcomeAction": "Take the tour",
"startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.",
"startOnboardingActionLabel": "Let's do it",
@@ -373,7 +368,5 @@
"automaticGameServerDialogStart": "Start server",
"gameResetDefaultsName": "Reset",
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
"gameResetDefaultsContent": "Reset",
"selectFile": "Select a file",
"reset": "Reset"
"gameResetDefaultsContent": "Reset"
}

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
@@ -18,9 +19,9 @@ import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/error.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/url_protocol.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:system_theme/system_theme.dart';
import 'package:url_protocol/url_protocol.dart';
import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart';
@@ -145,31 +146,30 @@ Future<Object?> _initVersion() async {
Future<Object?> _initUrlHandler() async {
try {
registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']);
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
return null;
}catch(error) {
return error;
}
}
Future<void> _initWindow() async {
void _initWindow() => doWhenWindowReady(() async {
try {
await SystemTheme.accentColor.load();
await windowManager.ensureInitialized();
await Window.initialize();
var settingsController = Get.find<SettingsController>();
var size = Size(settingsController.width, settingsController.height);
await windowManager.setSize(size);
appWindow.size = size;
var offsetX = settingsController.offsetX;
var offsetY = settingsController.offsetY;
if(offsetX != null && offsetY != null) {
final position = Offset(
if(offsetX != null && offsetY != null){
appWindow.position = Offset(
offsetX,
offsetY
);
await windowManager.setPosition(position);
}else {
await windowManager.setAlignment(Alignment.center);
appWindow.alignment = Alignment.center;
}
if(isWin11) {
@@ -182,9 +182,9 @@ Future<void> _initWindow() async {
}catch(error, stackTrace) {
onError(error, stackTrace, false);
}finally {
windowManager.show();
appWindow.show();
}
}
});
Future<List<Object>> _initStorage() async {
final errors = <Object>[];

View File

@@ -10,7 +10,7 @@ import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
class BackendController extends GetxController {
static const String storageName = "v2_backend_storage";
static const String storageName = "backend_storage";
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage? _storage;

View File

@@ -4,25 +4,28 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
class DllController extends GetxController {
static const String storageName = "v2_dll_storage";
static const String storageName = "dll_storage";
late final GetStorage? _storage;
late final String originalDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final Rx<UpdateTimer> timer;
late final TextEditingController beforeS20Mirror;
late final TextEditingController aboveS20Mirror;
late final TextEditingController url;
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
@@ -33,16 +36,15 @@ class DllController extends GetxController {
_storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.starfall);
backendDll = _createController("backend", InjectableDll.cobalt);
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
beforeS20Mirror = TextEditingController(text: _storage?.read("update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text));
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage?.write("update_url", url.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
@@ -59,14 +61,14 @@ class DllController extends GetxController {
void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.starfall);
backendDll.text = getDefaultDllPath(InjectableDll.cobalt);
memoryLeakDll.text = getDefaultDllPath(InjectableDll.memory);
}
void resetServer() {
gameServerPort.text = kDefaultGameServerPort;
timer.value = UpdateTimer.hour;
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
url.text = kRebootDownloadUrl;
status.value = UpdateStatus.waiting;
customGameServer.value = false;
timestamp.value = null;
@@ -107,15 +109,7 @@ class DllController extends GetxController {
duration: null
);
}
await Future.wait(
[
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text),
Future.delayed(const Duration(seconds: 1))
],
eagerError: false
);
timestamp.value = DateTime.now().millisecondsSinceEpoch;
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
infoBarEntry?.close();
if(!silent) {
@@ -132,18 +126,15 @@ class DllController extends GetxController {
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
infoBarEntry = showRebootInfoBar(
translations.downloadDllError(error.toString(), "reboot.dll"),
showRebootInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () async {
infoBarEntry?.close();
updateGameServerDll(
force: true,
silent: silent
);
},
onPressed: () => updateGameServerDll(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry),
)
);
@@ -153,21 +144,27 @@ class DllController extends GetxController {
}
}
(File, bool) getInjectableData(Version version, InjectableDll dll) {
(File, bool) getInjectableData(InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.reboot:
if(customGameServer.value) {
return (File(gameServerDll.text), true);
final file = File(gameServerDll.text);
if(file.existsSync()) {
return (file, true);
}
}
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
return (rebootDllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.starfall:
case InjectableDll.cobalt:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memory:
final memoryLeakFile = File(memoryLeakDll.text);
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
}
}
@@ -179,7 +176,7 @@ class DllController extends GetxController {
log("[DLL] File name: $fileName");
InfoBarEntry? entry;
try {
if (fileName.contains("reboot")) {
if (fileName == "reboot.dll") {
log("[DLL] Downloading reboot.dll...");
return await updateGameServerDll(
silent: silent
@@ -218,7 +215,7 @@ class DllController extends GetxController {
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(error.toString(), fileName),
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),

View File

@@ -8,7 +8,7 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
class GameController extends GetxController {
static const String storageName = "v2_game_storage";
static const String storageName = "game_storage";
late final GetStorage? _storage;
late final TextEditingController username;

View File

@@ -12,12 +12,10 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart';
class HostingController extends GetxController {
static const String storageName = "v2_hosting_storage";
static const String storageName = "hosting_storage";
late final GetStorage? _storage;
late final String uuid;
late final TextEditingController accountUsername;
late final TextEditingController accountPassword;
late final TextEditingController name;
late final FocusNode nameFocusNode;
late final TextEditingController description;
@@ -39,10 +37,6 @@ class HostingController extends GetxController {
_storage = appWithNoStorage ? null : GetStorage(storageName);
uuid = _storage?.read("uuid") ?? const Uuid().v4();
_storage?.write("uuid", uuid);
accountUsername = TextEditingController(text: _storage?.read("account_username") ?? kDefaultHostName);
accountUsername.addListener(() => _storage?.write("account_username", accountUsername.text));
accountPassword = TextEditingController(text: _storage?.read("account_password") ?? "");
accountPassword.addListener(() => _storage?.write("account_password", password.text));
name = TextEditingController(text: _storage?.read("name"));
name.addListener(() => _storage?.write("name", name.text));
description = TextEditingController(text: _storage?.read("description"));
@@ -62,34 +56,18 @@ class HostingController extends GetxController {
published = RxBool(false);
showPassword = RxBool(false);
instance = Rxn();
servers = Rxn();
_listenServers();
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_semaphore = Semaphore();
}
void _listenServers([int attempt = 0]) {
log("[SUPABASE] Listening...");
final supabase = Supabase.instance.client;
servers = Rxn();
supabase.from("hosting_v2")
.stream(primaryKey: ['id'])
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
.listen(
_onNewServer,
onError: (error) async {
log("[SUPABASE] Error: ${error}");
await Future.delayed(Duration(seconds: attempt * 5));
_listenServers(attempt + 1);
},
cancelOnError: true
);
}
void _onNewServer(Set<FortniteServer> event) {
log("[SUPABASE] New event: ${event}");
servers.value = event;
published.value = event.any((element) => element.id == uuid);
.listen((event) {
servers.value = event;
published.value = event.any((element) => element.id == uuid);
});
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_semaphore = Semaphore();
}
Future<void> publishServer(String author, String version) async {
@@ -158,8 +136,6 @@ class HostingController extends GetxController {
}
void reset() {
accountUsername.text = kDefaultHostName;
accountPassword.text = "";
name.text = "";
description.text = "";
showPassword.value = false;

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
@@ -13,12 +15,13 @@ import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
class SettingsController extends GetxController {
static const String storageName = "v2_settings_storage";
static const String storageName = "settings_storage";
late final GetStorage? _storage;
late final RxString language;
late final Rx<ThemeMode> themeMode;
late final RxBool firstRun;
late final RxBool debug;
late double width;
late double height;
late double? offsetX;
@@ -36,6 +39,7 @@ class SettingsController extends GetxController {
language.listen((value) => _storage?.write("language", value));
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
debug = RxBool(false);
}
void saveWindowSize(Size size) {

View File

@@ -300,7 +300,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _primaryButton => Button(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
),
onPressed: widget.onTap!,
child: Text(widget.text!),
@@ -308,7 +308,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _secondaryButton => Button(
style: widget.color != null ? ButtonStyle(
backgroundColor: WidgetStateProperty.all(widget.color!)
backgroundColor: ButtonState.all(widget.color!)
) : null,
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
child: Text(widget.text ?? translations.defaultDialogSecondaryAction),

View File

@@ -18,12 +18,9 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
}
lastError = exception.toString();
if(inDialog){
final context = pageKey.currentContext;
if(context != null) {
Navigator.of(context).pop(false);
inDialog = false;
}
final route = ModalRoute.of(pageKey.currentContext!);
if(route != null && !route.isCurrent){
Navigator.of(pageKey.currentContext!).pop(false);
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(

View File

@@ -17,7 +17,6 @@ import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart';
void startOnboarding() {
final gameController = Get.find<GameController>();
final settingsController = Get.find<SettingsController>();
settingsController.firstRun.value = false;
profileOverlayKey.currentState!.showOverlay(
@@ -28,7 +27,7 @@ void startOnboarding() {
label: translations.startOnboardingActionLabel,
onTap: () async {
onClose();
await showProfileForm(context, gameController.username, gameController.password);
await showProfileForm(context);
_promptPlayPage();
}
)
@@ -63,7 +62,7 @@ void _promptPlayVersion() {
onTap: () async {
onClose();
if(!hasBuilds) {
await VersionSelector.openDownloadDialog();
await VersionSelector.openDownloadDialog(closable: false);
}
_promptServerBrowserPage();
}
@@ -79,22 +78,6 @@ void _promptServerBrowserPage() {
context: context,
label: translations.promptServerBrowserPageActionLabel,
onTap: () {
onClose();
_promptHostAccount();
}
)
);
}
void _promptHostAccount() {
pageIndex.value = RebootPageType.host.index;
profileOverlayKey.currentState!.showOverlay(
text: translations.hostAccountText,
offset: Offset(27.5, 17.5),
actionBuilder: (context, onClose) => _buildActionButton(
context: context,
label: translations.hostAccountAction,
onTap: () async {
onClose();
_promptHostPage();
}
@@ -103,6 +86,7 @@ void _promptHostAccount() {
}
void _promptHostPage() {
pageIndex.value = RebootPageType.host.index;
pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptHostPageText,
actionBuilder: (context, onClose) => _buildActionButton(
@@ -355,7 +339,7 @@ Widget _buildActionButton({
required void Function() onTap,
}) => Button(
style: themed ? ButtonStyle(
backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor)
) : null,
child: Text(label),
onPressed: onTap

View File

@@ -6,11 +6,13 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
final GameController _gameController = Get.find<GameController>();
Future<bool> showProfileForm(BuildContext context) async{
final showPassword = RxBool(false);
final oldUsername = username.text;
final oldUsername = _gameController.username.text;
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
final oldPassword = password.text;
final oldPassword = _gameController.password.text;
final result = await showRebootDialog<bool?>(
builder: (context) => Obx(() => FormDialog(
content: Column(
@@ -23,17 +25,17 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
child: TextFormBox(
placeholder: translations.usernameOrEmailPlaceholder,
validator: (text) {
if(password.text.isEmpty) {
if(_gameController.password.text.isEmpty) {
return null;
}
if(EmailValidator.validate(username.text)) {
if(EmailValidator.validate(_gameController.username.text)) {
return null;
}
return translations.invalidEmail;
},
controller: username,
controller: _gameController.username,
autovalidateMode: AutovalidateMode.always,
enableSuggestions: true,
autofocus: true,
@@ -45,7 +47,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
label: translations.password,
child: TextFormBox(
placeholder: translations.passwordPlaceholder,
controller: password,
controller: _gameController.password,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
@@ -54,8 +56,8 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
suffix: Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
showPassword.value ? Icons.visibility_off : Icons.visibility,
@@ -85,7 +87,7 @@ Future<bool> showProfileForm(BuildContext context, TextEditingController usernam
return true;
}
username.text = oldUsername;
password.text = oldPassword;
_gameController.username.text = oldUsername;
_gameController.password.text = oldPassword;
return false;
}

View File

@@ -257,8 +257,8 @@ extension ServerControllerDialog on BackendController {
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular

View File

@@ -9,7 +9,6 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/util/types.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:universal_disk_space/universal_disk_space.dart';
import 'package:windows_taskbar/windows_taskbar.dart';
@@ -33,12 +32,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
final Rxn<FortniteBuild> _build = Rxn();
final RxnInt _timeLeft = RxnInt();
final Rxn<double> _progress = Rxn();
final RxInt _speed = RxInt(0);
late DiskSpace _diskSpace;
late Future<List<FortniteBuild>> _fetchFuture;
late Future _diskFuture;
Isolate? _isolate;
SendPort? _downloadPort;
Object? _error;
StackTrace? _stackTrace;
@@ -60,8 +59,9 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
}
void _cancelDownload() {
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
_downloadPort?.send(kStopBuildDownloadSignal);
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
_isolate?.kill(priority: Isolate.immediate);
}
@override
@@ -102,12 +102,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return ErrorDialog(
exception: _error ?? Exception(translations.unknownError),
stackTrace: _stackTrace,
errorMessageBuilder: (exception) {
var error = exception.toString();
error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error;
error = error.toLowerCase();
return translations.downloadVersionError(error);
}
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString())
);
case _DownloadStatus.done:
return InfoDialog(
@@ -156,7 +151,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
final communicationPort = ReceivePort();
communicationPort.listen((message) {
if(message is FortniteBuildDownloadProgress) {
_onProgress(build, message);
_onProgress(build, message.progress, message.minutesLeft, message.extracting);
}else if(message is SendPort) {
_downloadPort = message;
}else {
@@ -170,7 +165,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
);
final errorPort = ReceivePort();
errorPort.listen((message) => _onDownloadError(message, null));
await Isolate.spawn(
_isolate = await Isolate.spawn(
downloadArchiveBuild,
options,
onError: errorPort.sendPort,
@@ -210,24 +205,23 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
_stackTrace = stackTrace;
}
void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) {
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) {
if (!mounted) {
return;
}
if(message.progress >= 100 && message.extracting) {
if(progress >= 100 && extracting) {
_onDownloadComplete(build);
return;
}
_status.value = message.extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
if(message.progress >= 0) {
WindowsTaskbar.setProgress(message.progress.round(), 100);
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
if(progress >= 0) {
WindowsTaskbar.setProgress(progress.round(), 100);
}
_timeLeft.value = message.timeLeft;
_progress.value = message.progress;
_speed.value = message.speed;
_timeLeft.value = timeLeft;
_progress.value = progress;
}
Widget get _progressBody {
@@ -238,33 +232,31 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
Align(
alignment: Alignment.centerLeft,
child: Text(
_statusText,
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting,
style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start,
),
),
if(_progress.value != null && !_isAllocatingDiskSpace)
const SizedBox(
height: 8.0,
),
const SizedBox(
height: 8.0,
),
if(_progress.value != null && !_isAllocatingDiskSpace)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
translations.buildProgress((_progress.value ?? 0).round()),
style: FluentTheme.maybeOf(context)?.typography.body,
),
if(timeLeft != null)
Text(
translations.buildProgress((_progress.value ?? 0).round()),
translations.timeLeft(timeLeft),
style: FluentTheme.maybeOf(context)?.typography.body,
),
if(timeLeft != null)
Text(
translations.timeLeft(timeLeft),
style: FluentTheme.maybeOf(context)?.typography.body,
)
],
),
)
],
),
const SizedBox(
height: 8.0,
@@ -272,7 +264,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
SizedBox(
width: double.infinity,
child: ProgressBar(value: _isAllocatingDiskSpace ? null : _progress.value?.toDouble())
child: ProgressBar(value: (_progress.value ?? 0).toDouble())
),
const SizedBox(
@@ -282,26 +274,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
);
}
String get _statusText {
if (_status.value != _DownloadStatus.downloading) {
return translations.extracting;
}
if (_progress.value == null) {
return translations.startingDownload;
}
if (_speed.value == 0) {
return translations.allocatingSpace;
}
return translations.downloading;
}
bool get _isAllocatingDiskSpace => _status.value == _DownloadStatus.downloading && _speed.value == 0;
Widget _buildFormBody(List<FortniteBuild> builds) {
return Column(
Widget _buildFormBody(List<FortniteBuild> builds) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -319,8 +292,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
controller: _pathController,
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
folder: true,
allowNavigator: true
folder: true
),
const SizedBox(
@@ -328,7 +300,6 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
)
],
);
}
String? _checkGameFolder(text) {
if (text == null || text.isEmpty) {

View File

@@ -1,5 +1,6 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
@@ -34,6 +35,7 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
_buildDebugInfo(),
Expanded(
child: _listView
)
@@ -45,6 +47,7 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
_buildDebugInfo(),
Expanded(
child: Column(
children: [
@@ -96,6 +99,35 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
);
});
Widget _buildDebugInfo() => Obx(() {
if(!_settingsController.debug.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("Debug mode is enabled"),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text( "• Automatic dll injection is disabled\n"
"• The game server cannot start automatically\n"
"• The game server runs in a normal window")
),
onClose: () {
_settingsController.debug.value = false;
},
),
)
);
});
ListView get _listView => ListView.builder(
itemCount: settings.length,
itemBuilder: (context, index) => settings[index],

View File

@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
@@ -152,9 +153,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
contentWidth: null,
content: Row(
children: [
Obx(() => Text(
Text(
_backendController.detached.value ? translations.on : translations.off
)),
),
const SizedBox(
width: 16.0
),

View File

@@ -276,8 +276,8 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
_filterControllerStream.add("");
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.all(Border())
backgroundColor: ButtonState.all(Colors.transparent),
shape: ButtonState.all(Border())
),
child: _searchBarIconData
);

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage;
@@ -26,7 +27,6 @@ import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart';
import 'package:reboot_launcher/src/widget/title_bar.dart';
import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart';
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
@@ -58,6 +58,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override
void initState() {
super.initState();
windowManager.setPreventClose(true);
windowManager.addListener(this);
_syncPageViewWithNavigator();
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -110,7 +111,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
return;
}
final result = await pingGameServer(address);
var result = await pingGameServer(address);
if(result) {
return;
}
@@ -134,12 +135,13 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
dllsDirectory.createSync(recursive: true);
}
final dummy = Version.parse("1");
final dummyS20 = Version.parse("20");
for(final injectable in InjectableDll.values) {
_downloadDll(dummy, injectable);
if(injectable.isVersionDependent) {
_downloadDll(dummyS20, injectable);
final (file, custom) = _dllController.getInjectableData(injectable);
if(!custom) {
_dllController.downloadCriticalDllInteractive(
file.path,
silent: true
);
}
}
@@ -148,22 +150,12 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
}));
}
void _downloadDll(Version version, InjectableDll injectable) {
final (file, custom) = _dllController.getInjectableData(version, injectable);
if(!custom) {
_dllController.downloadCriticalDllInteractive(
file.path,
silent: false
);
}
}
@override
void onWindowClose() async {
try {
await _hostingController.discardServer();
}catch(error) {
log("[HOSTING] Cannot discard server: $error");
}finally {
exit(0); // Force closing
}
}
@@ -228,18 +220,14 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override
void onWindowResized() {
_settingsController.saveWindowSize(appWindow.size);
_focused.value = true;
windowManager.getSize().then((size) {
_settingsController.saveWindowSize(size);
});
}
@override
void onWindowMoved() {
_settingsController.saveWindowOffset(appWindow.position);
_focused.value = true;
windowManager.getPosition().then((position) {
_settingsController.saveWindowOffset(position);
});
}
@override
@@ -310,46 +298,48 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
});
}
Widget _buildBody() => Expanded(
child: Padding(
padding: EdgeInsets.only(
left: HomePage.kDefaultPadding,
right: HomePage.kDefaultPadding * 2,
top: HomePage.kDefaultPadding,
bottom: HomePage.kDefaultPadding * 2
),
child: Column(
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 1000
),
child: Center(
child: Column(
children: [
_buildBodyHeader(),
const SizedBox(height: 24.0),
Expanded(
child: Stack(
fit: StackFit.loose,
children: [
_buildBodyContent(),
InfoBarArea(
key: infoBarAreaKey
)
],
)
),
],
Widget _buildBody() {
return Expanded(
child: Padding(
padding: EdgeInsets.only(
left: HomePage.kDefaultPadding,
right: HomePage.kDefaultPadding * 2,
top: HomePage.kDefaultPadding,
bottom: HomePage.kDefaultPadding * 2
),
child: Column(
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 1000
),
child: Center(
child: Column(
children: [
_buildBodyHeader(),
const SizedBox(height: 24.0),
Expanded(
child: Stack(
fit: StackFit.loose,
children: [
_buildBodyContent(),
InfoBarArea(
key: infoBarAreaKey
)
],
)
),
],
),
),
),
),
)
],
)
),
);
)
],
)
),
);
}
Widget _buildBodyContent() => PageView.builder(
controller: _pageController,
@@ -461,12 +451,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() {
pageIndex.value;
return ProfileWidget(
overlayKey: profileOverlayKey
);
}),
ProfileWidget(
overlayKey: profileOverlayKey
),
_autoSuggestBox,
const SizedBox(height: 12.0),
_buildNavigationTrail()
@@ -513,7 +500,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor(
FluentTheme.of(context),
pageIndex.value == index ? {WidgetState.hovered} : states,
pageIndex.value == index ? {ButtonStates.hovering} : states,
transparentWhenNone: true,
),
borderRadius: BorderRadius.all(Radius.circular(6.0))
@@ -542,12 +529,12 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
stream: pagesController.stream,
builder: (context, _) => Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(
padding: ButtonState.all(const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0
)),
backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: WidgetStateProperty.all(Border())
backgroundColor: ButtonState.all(Colors.transparent),
shape: ButtonState.all(Border())
),
onPressed: appStack.isEmpty && !inDialog ? null : () {
if(inDialog) {
@@ -569,7 +556,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
);
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: windowManager.maximizeOrRestore,
onDoubleTap: appWindow.maximizeOrRestore,
onHorizontalDragStart: (_) => windowManager.startDragging(),
onVerticalDragStart: (_) => windowManager.startDragging()
);

View File

@@ -53,6 +53,7 @@ class HostPage extends RebootPage {
class _HostingPageState extends RebootPageState<HostPage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
@@ -81,9 +82,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
List<Widget> get settings => [
_information,
buildVersionSelector(
key: hostVersionOverlayTargetKey
key: hostVersionOverlayTargetKey
),
_options,
_internalFiles,
_share,
_resetDefaults
];
@@ -153,8 +155,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
suffix: Button(
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
shape: ButtonState.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent)
),
child: Icon(
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
@@ -173,9 +175,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: null,
content: Obx(() => Row(
children: [
Obx(() => Text(
Text(
_hostingController.discoverable.value ? translations.on : translations.off
)),
),
const SizedBox(
width: 16.0
),
@@ -219,11 +221,12 @@ class _HostingPageState extends RebootPageState<HostPage> {
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_hostingController.type.value.translatedName),
leading: Text(_settingsController.debug.value ? GameServerType.window.translatedName : _hostingController.type.value.translatedName),
items: GameServerType.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _hostingController.type.value = entry
)).toList()
)).toList(),
disabled: _settingsController.debug.value
)),
),
SettingTile(
@@ -235,9 +238,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: null,
content: Row(
children: [
Obx(() => Text(
Text(
_hostingController.autoRestart.value ? translations.on : translations.off
)),
),
const SizedBox(
width: 16.0
),
@@ -268,6 +271,159 @@ class _HostingPageState extends RebootPageState<HostPage> {
],
);
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsServerName),
subtitle: Text(translations.settingsServerSubtitle),
children: [
SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTypeName),
subtitle: Text(translations.settingsServerTypeDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value),
onPressed: () {
final oldValue = _dllController.customGameServer.value;
if(oldValue == entry.key) {
return;
}
_dllController.customGameServer.value = entry.key;
_dllController.infoBarEntry?.close();
if(!entry.key) {
_dllController.updateGameServerDll(
force: true
);
}
}
)).toList()
))
),
Obx(() {
if(!_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return createFileSetting(
title: translations.settingsServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.gameServerDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.reboot);
_dllController.gameServerDll.text = path;
_dllController.downloadCriticalDllInteractive(path);
}
);
}),
Obx(() {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.url,
validator: _checkUpdateUrl
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: ButtonState.all(EdgeInsets.zero)
),
onPressed: () => _dllController.url.text = kRebootDownloadUrl,
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
)
);
}),
Obx(() {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
content: Row(
children: [
Expanded(
child: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_dllController.timer.value = entry;
_dllController.infoBarEntry?.close();
_dllController.updateGameServerDll(
force: true
);
}
)).toList()
)),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: ButtonState.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
)
],
)
);
})
],
);
String? _checkUpdateUrl(String? text) {
if (text == null || text.isEmpty) {
return translations.emptyURL;
}
return null;
}
SettingTile get _share => SettingTile(
icon: Icon(
FluentIcons.link_24_regular
@@ -338,8 +494,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
try {
_hostingController.publishServer(
_hostingController.accountUsername.text,
_hostingController.instance.value!.version.toString()
_gameController.username.text,
_hostingController.instance.value!.versionName
);
} catch(error) {
_showCannotUpdateGameServer(error);
@@ -373,4 +529,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
severity: InfoBarSeverity.success,
duration: infoBarLongDuration
);
}
extension _UpdateTimerExtension on UpdateTimer {
String get text {
if (this == UpdateTimer.never) {
return translations.updateGameServerDllNever;
}
return translations.updateGameServerDllEvery(name);
}
}

View File

@@ -1,12 +1,17 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/dll_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/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
@@ -33,7 +38,9 @@ class PlayPage extends RebootPage {
}
class _PlayPageState extends RebootPageState<PlayPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final GameController _gameController = Get.find<GameController>();
final DllController _dllController = Get.find<DllController>();
@override
Widget? get button => LaunchButton(
@@ -48,9 +55,50 @@ class _PlayPageState extends RebootPageState<PlayPage> {
key: gameVersionOverlayTargetKey
),
_options,
_internalFiles,
_resetDefaults
];
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
),
createFileSetting(
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.cobalt);
_dllController.backendDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
),
createFileSetting(
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.memory);
_dllController.memoryLeakDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
),
],
);
SettingTile get _options => SettingTile(
icon: Icon(
FluentIcons.options_24_regular
@@ -81,6 +129,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
content: Button(
onPressed: () => showResetDialog(() {
_gameController.reset();
_dllController.resetGame();
}),
child: Text(translations.gameResetDefaultsContent),
)

View File

@@ -4,13 +4,12 @@ import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -35,7 +34,6 @@ class SettingsPage extends RebootPage {
class _SettingsPageState extends RebootPageState<SettingsPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
@override
Widget? get button => null;
@@ -44,235 +42,10 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
List<Widget> get settings => [
_language,
_theme,
_internalFiles,
_debugMode,
_installationDirectory,
];
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
),
createFileSetting(
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.starfall);
_dllController.backendDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
),
_internalFilesServerType,
_internalFilesUpdateTimer,
_internalFilesServerSource,
_internalFilesNewServerSource,
],
);
Widget get _internalFilesServerType => SettingTile(
icon: Icon(
FluentIcons.games_24_regular
),
title: Text(translations.settingsServerTypeName),
subtitle: Text(translations.settingsServerTypeDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value),
onPressed: () {
final oldValue = _dllController.customGameServer.value;
if(oldValue == entry.key) {
return;
}
_dllController.customGameServer.value = entry.key;
_dllController.infoBarEntry?.close();
if(!entry.key) {
_dllController.updateGameServerDll(
force: true
);
}
}
)).toList()
))
);
Widget get _internalFilesServerSource => Obx(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerOldMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _dllController.updateGameServerDll(force: true),
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
)
);
}else {
return createFileSetting(
title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.gameServerDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.reboot);
_dllController.gameServerDll.text = path;
_dllController.downloadCriticalDllInteractive(path);
}
);
}
});
Widget get _internalFilesNewServerSource => Obx(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerNewMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _dllController.updateGameServerDll(force: true),
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.aboveS20Mirror.text = kRebootBelowS20DownloadUrl;
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
)
);
}else {
return const SizedBox();
}
});
Widget get _internalFilesUpdateTimer => Obx(() {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_dllController.timer.value = entry;
_dllController.infoBarEntry?.close();
_dllController.updateGameServerDll(
force: true
);
}
)).toList()
))
);
});
SettingTile get _language => SettingTile(
icon: Icon(
FluentIcons.local_language_24_regular
@@ -327,6 +100,29 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
child: Text(translations.settingsUtilsInstallationDirectoryContent),
)
);
SettingTile get _debugMode => SettingTile(
icon: Icon(
FluentIcons.developer_board_24_regular
),
title: Text("Debug mode"),
subtitle: Text("Whether the launcher should disable automatic features for troubleshooting"),
contentWidth: null,
content: Row(
children: [
Text(
_settingsController.debug.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
Obx(() => ToggleSwitch(
checked: _settingsController.debug.value,
onChanged: (value) => _settingsController.debug.value = value
))
],
)
);
}
extension _ThemeModeExtension on ThemeMode {
@@ -340,14 +136,4 @@ extension _ThemeModeExtension on ThemeMode {
return translations.light;
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
String get text {
if (this == UpdateTimer.never) {
return translations.updateGameServerDllNever;
}
return translations.updateGameServerDllEvery(name);
}
}

View File

@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@@ -6,26 +5,39 @@ import 'package:reboot_common/common.dart';
const Duration _timeout = Duration(seconds: 5);
Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
final completer = Completer<bool>();
final start = DateTime.now();
(() async {
while (!completer.isCompleted && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds) {
final result = await pingGameServer(address);
if(result) {
completer.complete(true);
}else {
await Future.delayed(_timeout);
Future<bool> pingGameServer(String address, {Duration? timeout}) async {
Future<bool> ping(String hostname, int port) async {
log("[MATCHMAKER] Pinging $hostname:$port");
RawDatagramSocket? socket;
try {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
await for (final event in socket) {
log("[MATCHMAKER] Event: $event");
switch(event) {
case RawSocketEvent.read:
log("[MATCHMAKER] Success");
return true;
case RawSocketEvent.write:
log("[MATCHMAKER] Sending data");
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
socket.send(dataToSend, InternetAddress(hostname), port);
case RawSocketEvent.readClosed:
case RawSocketEvent.closed:
return false;
}
}
}
if(!completer.isCompleted) {
completer.complete(false);
}
})();
return completer;
}
Future<bool> pingGameServer(String address) async {
return false;
}catch(error) {
log("[MATCHMAKER] Error: $error");
return false;
}finally {
socket?.close();
}
}
final start = DateTime.now();
var firstTime = true;
final split = address.split(":");
var hostname = split[0];
if(isLocalHost(hostname)) {
@@ -33,37 +45,19 @@ Future<bool> pingGameServer(String address) async {
}
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
return await _ping(hostname, port)
.timeout(_timeout, onTimeout: () => false);
}
Future<bool> _ping(String hostname, int port) async {
log("[MATCHMAKER] Pinging $hostname:$port");
RawDatagramSocket? socket;
try {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
await for (final event in socket) {
log("[MATCHMAKER] Event: $event");
switch(event) {
case RawSocketEvent.read:
log("[MATCHMAKER] Success");
return true;
case RawSocketEvent.write:
log("[MATCHMAKER] Sending data");
final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA==");
socket.send(dataToSend, InternetAddress(hostname), port);
case RawSocketEvent.readClosed:
case RawSocketEvent.closed:
return false;
}
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
final result = await ping(hostname, port)
.timeout(_timeout, onTimeout: () => false);
if(result) {
return true;
}
return false;
}catch(error) {
log("[MATCHMAKER] Error: $error");
return false;
}finally {
socket?.close();
if(firstTime) {
firstTime = false;
}else {
await Future.delayed(_timeout);
}
}
return false;
}

View File

@@ -7,7 +7,6 @@ import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/scheduler.dart';
import 'package:win32/win32.dart';
import 'package:window_manager/window_manager.dart';
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -25,13 +24,10 @@ bool get isWin11 {
return intBuild != null && intBuild > 22000;
}
Future<String?> openFolderPicker(String title) async {
FilePicker.platform = FilePickerWindows();
return await FilePicker.platform.getDirectoryPath(dialogTitle: title);
}
Future<String?> openFolderPicker(String title) async =>
await FilePicker.platform.getDirectoryPath(dialogTitle: title);
Future<String?> openFilePicker(String extension) async {
FilePicker.platform = FilePickerWindows();
var result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowMultiple: false,
@@ -97,7 +93,7 @@ class IVirtualDesktop extends IUnknown {
throw WindowsException(code);
}
return _convertFromHString(result.value);
return convertFromHString(result.value);
}
}
@@ -284,7 +280,7 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
HRESULT Function(Pointer, COMObject, Int8)>>>()
.value
.asFunction<int Function(Pointer, COMObject, int)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName));
if (code != 0) {
throw WindowsException(code);
}
@@ -373,7 +369,7 @@ List<int> _getHWnds(int pid, String? excludedWindowName) {
result.ref.excluded = excludedWindowName.toNativeUtf16();
}
EnumWindows(Pointer.fromFunction<WNDENUMPROC>(_filter, TRUE), result.address);
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
final length = result.ref.HWndLength;
final HWndsPointer = result.ref.HWnd;
if(HWndsPointer == nullptr) {
@@ -401,7 +397,7 @@ class VirtualDesktopManager {
}
final hr = CoInitializeEx(
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (FAILED(hr)) {
throw WindowsException(hr);
}
@@ -472,23 +468,3 @@ class VirtualDesktopManager {
void setDesktopName(IVirtualDesktop desktop, String newName) =>
windowManager.setDesktopName(desktop, newName);
}
String _convertFromHString(int hstring) =>
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
int _convertToHString(String string) {
final hString = calloc<HSTRING>();
final stringPtr = string.toNativeUtf16();
try {
final hr = WindowsCreateString(stringPtr, string.length, hString);
if (FAILED(hr)) throw WindowsException(hr);
return hString.value;
} finally {
free(stringPtr);
free(hString);
}
}
extension WindowManagerExtension on WindowManager {
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
}

View File

@@ -5,15 +5,4 @@ extension IterableExtension<E> on Iterable<E> {
}
return null;
}
}
extension StringExtension on String {
String? after(String leading) {
final index = indexOf(leading);
if(index == -1) {
return null;
}
return substring(index + leading.length);
}
}

View File

@@ -1,63 +0,0 @@
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
final _hive = HKEY_CURRENT_USER;
void registerUrlProtocol(String scheme, {String? executable, List<String>? arguments}) {
final prefix = _regPrefix(scheme);
final capitalized = scheme[0].toUpperCase() + scheme.substring(1);
final args = _getArguments(arguments).map((a) => _sanitize(a));
final cmd =
'${executable ?? Platform.resolvedExecutable} ${args.join(' ')}';
_regCreateStringKey(_hive, prefix, '', 'URL:$capitalized');
_regCreateStringKey(_hive, prefix, 'URL Protocol', '');
_regCreateStringKey(_hive, prefix + '\\shell\\open\\command', '', cmd);
}
void unregisterUrlProtocol(String scheme) {
final txtKey = TEXT(_regPrefix(scheme));
try {
RegDeleteTree(HKEY_CURRENT_USER, txtKey);
} finally {
free(txtKey);
}
}
String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme';
int _regCreateStringKey(int hKey, String key, String valueName, String data) {
final txtKey = TEXT(key);
final txtValue = TEXT(valueName);
final txtData = TEXT(data);
try {
return RegSetKeyValue(
hKey,
txtKey,
txtValue,
REG_VALUE_TYPE.REG_SZ,
txtData,
txtData.length * 2 + 2,
);
} finally {
free(txtKey);
free(txtValue);
free(txtData);
}
}
String _sanitize(String value) {
value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"');
return '"$value"';
}
List<String> _getArguments(List<String>? arguments) {
if (arguments == null) return ['%s'];
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
throw ArgumentError('arguments must contain at least 1 instance of "%s"');
}
return arguments;
}

View File

@@ -19,10 +19,10 @@ class FileSelector extends StatefulWidget {
required this.controller,
required this.validator,
required this.folder,
required this.allowNavigator,
this.label,
this.extension,
this.validatorMode,
this.allowNavigator = true,
Key? key})
: assert(folder || extension != null, "Missing extension for file selector"),
super(key: key);

View File

@@ -1,121 +1,70 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' as fluentIcons show FluentIcons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8;
// FIXME: If the user clicks on the reset button, the text field checker won't be called
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
final obx = RxString(controller.text);
controller.addListener(() => obx.value = controller.text);
final selecting = RxBool(false);
return SettingTile(
icon: Icon(
FluentIcons.document_24_regular
),
title: Text(title),
subtitle: Text(description),
contentWidth: SettingTile.kDefaultContentWidth + _kButtonDimensions,
content: Row(
return SettingTile(
icon: Icon(
FluentIcons.document_24_regular
),
title: Text(title),
subtitle: Text(description),
content: Row(
children: [
Expanded(
child: FileSelector(
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
controller: controller,
validator: _checkDll,
extension: "dll",
folder: false,
validatorMode: AutovalidateMode.always,
allowNavigator: false,
Expanded(
child: FileSelector(
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
controller: controller,
validator: _checkDll,
extension: "dll",
folder: false,
validatorMode: AutovalidateMode.always
),
),
),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.selectFile,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _onPressed(selecting, controller),
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
fluentIcons.FluentIcons.open_folder_horizontal
),
)
const SizedBox(width: 8.0),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
),
),
)),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.reset,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
padding: ButtonState.all(EdgeInsets.zero)
),
onPressed: onReset,
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
),
),
))
))
],
)
);
}
void _onPressed(RxBool selecting, TextEditingController controller) {
if(selecting.value){
return;
}
selecting.value = true;
compute(openFilePicker, "dll")
.then((value) => _updateText(controller, value))
.then((_) => selecting.value = false);
}
void _updateText(TextEditingController controller, String? value) {
final text = value ?? controller.text;
controller.text = text;
controller.selection = TextSelection.collapsed(offset: text.length);
)
);
}
String? _checkDll(String? text) {
if (text == null || text.isEmpty) {
return translations.invalidDllPath;
}
if (text == null || text.isEmpty) {
return translations.invalidDllPath;
}
final file = File(text);
if (!file.existsSync()) {
return translations.dllDoesNotExist;
}
final file = File(text);
if (!file.existsSync()) {
return translations.dllDoesNotExist;
}
if (!text.endsWith(".dll")) {
return translations.invalidDllExtension;
}
if (!text.endsWith(".dll")) {
return translations.invalidDllExtension;
}
return null;
return null;
}

View File

@@ -12,6 +12,7 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
@@ -21,7 +22,6 @@ import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart';
class LaunchButton extends StatefulWidget {
final bool host;
@@ -41,11 +41,12 @@ class _LaunchButtonState extends State<LaunchButton> {
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final DllController _dllController = Get.find<DllController>();
final SettingsController _settingsController = Get.find<SettingsController>();
InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation;
Completer? _pingOperation;
CancelableOperation? _pingOperation;
IVirtualDesktop? _virtualDesktop;
@override
@@ -94,7 +95,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(version.content, injectable, host) == null) {
if(await _getDllFileOrStop(injectable, host) == null) {
return;
}
}
@@ -120,7 +121,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
log("[${host ? 'HOST' : 'GAME'}] Backend works");
final serverType = _hostingController.type.value;
final serverType = _settingsController.debug.value ? GameServerType.window : _hostingController.type.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
@@ -158,6 +159,11 @@ class _LaunchButtonState extends State<LaunchButton> {
return null;
}
if(_settingsController.debug.value) {
log("[${host ? 'HOST' : 'GAME'}] The user is on debug mode, not asking for auto server");
return null;
}
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
return null;
@@ -229,7 +235,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance(
version: version.content,
versionName: version.content.toString(),
gamePid: gameProcess,
launcherPid: launcherProcess,
eacPid: eacProcess,
@@ -242,7 +248,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{
_gameController.instance.value = instance;
}
await _injectOrShowError(InjectableDll.starfall, host);
await _injectOrShowError(InjectableDll.cobalt, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance;
}
@@ -250,8 +256,8 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs(
host ? _hostingController.accountUsername.text : _gameController.username.text,
host ? _hostingController.accountPassword.text :_gameController.password.text,
_gameController.username.text,
_gameController.password.text,
host,
hostType,
false,
@@ -274,7 +280,13 @@ class _LaunchButtonState extends State<LaunchButton> {
line: line,
host: host,
onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError),
onTokenError: () {
if(_settingsController.debug.value) {
log("[PROCESS] Ignoring token error because debug mode is on");
}else {
_onStop(reason: _StopReason.tokenError);
}
},
onBuildCorrupted: () {
if(instance == null) {
return;
@@ -398,6 +410,7 @@ class _LaunchButtonState extends State<LaunchButton> {
if(instance != null && !instance.launched) {
instance.launched = true;
instance.tokenError = false;
await _injectOrShowError(InjectableDll.memory, host);
if(!host){
await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected();
@@ -436,12 +449,11 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
final gameServerPort = _dllController.gameServerPort.text;
final pingOperation = pingGameServerOrTimeout(
this._pingOperation = await CancelableOperation.fromFuture(pingGameServer(
"127.0.0.1:$gameServerPort",
const Duration(minutes: 2)
);
this._pingOperation = pingOperation;
final localPingResult = await pingOperation.future;
timeout: const Duration(minutes: 2)
));
final localPingResult = (await _pingOperation?.value) ?? false;
_gameServerInfoBar?.close();
if (!localPingResult) {
showRebootInfoBar(
@@ -463,8 +475,8 @@ class _LaunchButtonState extends State<LaunchButton> {
}
await _hostingController.publishServer(
_hostingController.accountUsername.text,
_hostingController.instance.value!.version.toString(),
_gameController.username.text,
_hostingController.instance.value!.versionName,
);
showRebootInfoBar(
translations.gameServerStarted,
@@ -484,17 +496,18 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
final publicIp = await Ipify.ipv4();
final available = await pingGameServer("$publicIp:$gameServerPort");
if(available) {
_gameServerInfoBar?.close();
this._pingOperation = CancelableOperation.fromFuture(pingGameServer("$publicIp:$gameServerPort"));
final externalResult = (await _pingOperation?.value) ?? false;
if (externalResult) {
return true;
}
final pingOperation = pingGameServerOrTimeout(
_gameServerInfoBar?.close();
this._pingOperation = CancelableOperation.fromFuture(pingGameServer(
"$publicIp:$gameServerPort",
const Duration(days: 365)
);
this._pingOperation = pingOperation;
timeout: const Duration(days: 365)
));
final future = await _pingOperation?.value ?? false;
_gameServerInfoBar = showRebootInfoBar(
translations.checkGameServerFixMessage(gameServerPort),
action: Button(
@@ -505,9 +518,7 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null,
loading: true
);
final result = await pingOperation.future;
_gameServerInfoBar?.close();
return result;
return await future;
}finally {
_gameServerInfoBar?.close();
}
@@ -515,13 +526,8 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
if(host == null) {
try {
_pingOperation?.complete(false);
}catch(_) {
// Ignore: might be running, don't bother checking
} finally {
_pingOperation = null;
}
await _pingOperation?.cancel();
_pingOperation = null;
await _operation?.cancel();
_operation = null;
_backendController.cancelInteractive();
@@ -529,6 +535,9 @@ class _LaunchButtonState extends State<LaunchButton> {
host = host ?? widget.host;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance == null) {
return;
}
if(host){
_hostingController.instance.value = null;
@@ -552,11 +561,11 @@ class _LaunchButtonState extends State<LaunchButton> {
}
if(reason == _StopReason.normal) {
instance?.launched = true;
instance.launched = true;
}
instance?.kill();
final child = instance?.child;
instance.kill();
final child = instance.child;
if(child != null) {
await _onStop(
reason: reason,
@@ -593,7 +602,7 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.exitCode:
if(instance != null && !instance.launched) {
if(!instance.launched) {
showRebootInfoBar(
translations.corruptedVersionError,
severity: InfoBarSeverity.error,
@@ -603,9 +612,9 @@ class _LaunchButtonState extends State<LaunchButton> {
break;
case _StopReason.corruptedVersionError:
showRebootInfoBar(
translations.corruptedVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
translations.corruptedVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
@@ -629,13 +638,13 @@ class _LaunchButtonState extends State<LaunchButton> {
case _StopReason.tokenError:
_backendController.stop();
showRebootInfoBar(
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
translations.tokenError(instance.injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
break;
case _StopReason.crash:
@@ -665,7 +674,7 @@ class _LaunchButtonState extends State<LaunchButton> {
try {
final gameProcess = instance.gamePid;
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
final dllPath = await _getDllFileOrStop(injectable, hosting);
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
if(dllPath == null) {
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
@@ -678,6 +687,10 @@ class _LaunchButtonState extends State<LaunchButton> {
}
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
if(_settingsController.debug.value) {
return;
}
await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
@@ -692,9 +705,9 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final (file, customDll) = _dllController.getInjectableData(version, injectable);
final (file, customDll) = _dllController.getInjectableData(injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) {
log("[${host ? 'HOST' : 'GAME'}] Path exists");
@@ -714,7 +727,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _dllController.downloadCriticalDllInteractive(file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(version, injectable, host, true);
return _getDllFileOrStop(injectable, host, true);
}
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
@@ -725,32 +738,32 @@ class _LaunchButtonState extends State<LaunchButton> {
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
duration: null,
action: Obx(() {
if(_hostingController.started.value || linkedHosting) {
return const SizedBox.shrink();
}
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
duration: null,
action: Obx(() {
if(_settingsController.debug.value || _hostingController.started.value || linkedHosting) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 2.0
),
child: Button(
onPressed: () async {
_backendController.joinLocalhost();
if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
_gameClientInfoBar?.close();
_showLaunchingGameClientWidget(version, hostType, true);
}
},
child: Text(translations.startGameServer),
),
);
})
);
return Padding(
padding: const EdgeInsets.only(
bottom: 2.0
),
child: Button(
onPressed: () async {
_backendController.joinLocalhost();
if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
_gameClientInfoBar?.close();
_showLaunchingGameClientWidget(version, hostType, true);
}
},
child: Text(translations.startGameServer),
),
);
})
);
}
}

View File

@@ -24,22 +24,19 @@ class InfoBarAreaState extends State<InfoBarArea> {
}
@override
Widget build(BuildContext context) => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Obx(() => Padding(
padding: EdgeInsets.only(
bottom: hasPageButton ? 72.0 : 16.0
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: _children.value.map((child) => Padding(
padding: EdgeInsets.only(
top: 12.0
),
child: child
)).toList(growable: false)
),
))
);
Widget build(BuildContext context) => Obx(() => Padding(
padding: EdgeInsets.only(
bottom: hasPageButton ? 72.0 : 16.0
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: _children.value.map((child) => Padding(
padding: EdgeInsets.only(
top: 12.0
),
child: child
)).toList(growable: false)
),
));
}

View File

@@ -2,11 +2,8 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
class ProfileWidget extends StatefulWidget {
final GlobalKey<OverlayTargetState> overlayKey;
@@ -18,7 +15,6 @@ class ProfileWidget extends StatefulWidget {
class _ProfileWidgetState extends State<ProfileWidget> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
@override
Widget build(BuildContext context) => OverlayTarget(
@@ -26,7 +22,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
child: HoverButton(
margin: const EdgeInsets.all(8.0),
onPressed: () async {
if(await showProfileForm(context, _username, _password)) {
if(await showProfileForm(context)) {
setState(() {});
}
},
@@ -61,7 +57,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_usernameLabel,
_username,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w600
@@ -69,7 +65,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
maxLines: 1
),
Text(
_emailLabel,
_email,
textAlign: TextAlign.start,
style: const TextStyle(
fontWeight: FontWeight.w100
@@ -85,8 +81,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
),
);
String get _usernameLabel {
final username = _username.text;
String get _username {
var username = _gameController.username.text;
if(username.isEmpty) {
return kDefaultPlayerName;
}
@@ -100,8 +96,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
return result.substring(0, 1).toUpperCase() + result.substring(1);
}
String get _emailLabel {
final username = _username.text;
String get _email {
var username = _gameController.username.text;
if(username.isEmpty) {
return "$kDefaultPlayerName@projectreboot.dev";
}
@@ -112,7 +108,4 @@ class _ProfileWidgetState extends State<ProfileWidget> {
return "$username@projectreboot.dev".toLowerCase();
}
TextEditingController get _username => pageIndex.value == RebootPageType.host.index ? _hostingController.accountUsername : _gameController.username;
TextEditingController get _password => pageIndex.value == RebootPageType.host.index ? _hostingController.accountPassword : _gameController.password;
}

View File

@@ -1,6 +1,5 @@
import 'package:bitsdojo_window/bitsdojo_window.dart' show appWindow;
import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:window_manager/window_manager.dart';
import 'title_bar_icons.dart';
import 'title_bar_mouse.dart';
@@ -133,7 +132,7 @@ class MinimizeWindowButton extends WindowButton {
animate: animate ?? false,
iconBuilder: (buttonContext) =>
MinimizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => windowManager.minimize());
onPressed: onPressed ?? () => appWindow.minimize());
}
class MaximizeWindowButton extends WindowButton {
@@ -149,7 +148,7 @@ class MaximizeWindowButton extends WindowButton {
iconBuilder: (buttonContext) =>
MaximizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ??
() => windowManager.maximizeOrRestore());
() => appWindow.maximizeOrRestore());
}
final _defaultCloseButtonColors = WindowButtonColors(
@@ -170,5 +169,5 @@ class CloseWindowButton extends WindowButton {
animate: animate ?? false,
iconBuilder: (buttonContext) =>
CloseIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => windowManager.close());
onPressed: onPressed ?? () => appWindow.close());
}

View File

@@ -15,11 +15,11 @@ import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key);
static Future<void> openDownloadDialog() => showRebootDialog<bool>(
static Future<void> openDownloadDialog({bool closable = true}) => showRebootDialog<bool>(
builder: (context) => AddVersionDialog(
closable: true,
closable: closable,
),
dismissWithEsc: true
dismissWithEsc: closable
);
@override

View File

@@ -1,6 +1,6 @@
name: reboot_launcher
description: Graphical User Interface for Project Reboot
version: "10.0.0"
version: "9.2.6"
publish_to: 'none'
@@ -17,49 +17,53 @@ dependencies:
path: ./../common
# Windows UI 3
fluent_ui: ^4.9.1
fluent_ui: ^4.8.7
flutter_acrylic:
path: ./dependencies/flutter_acrylic
fluentui_system_icons: ^1.1.258
system_theme: ^3.1.1
fluentui_system_icons: ^1.1.238
system_theme: ^2.0.0
skeletons:
git:
url: https://github.com/talok/skeletons
ref: main
# Window management
window_manager: ^0.4.2
bitsdojo_window: ^0.1.5
window_manager: ^0.3.8
# Extract zip archives (for example the reboot.zip)
archive: ^3.6.1
archive: ^3.3.1
# Cryptographic functions
crypto: ^3.0.2
bcrypt: ^1.1.3
pointycastle: ^3.9.1
pointycastle: ^3.7.3
# Async helpers
async: ^2.11.0
async: ^2.8.2
sync: ^0.3.0
# State management
get: ^4.6.6
get: ^4.6.5
# Native utilities
clipboard: ^0.1.3
app_links: ^6.3.2
app_links: ^6.0.2
url_protocol: ^1.0.0
windows_taskbar: ^1.1.2
file_picker: ^8.1.2
url_launcher: ^6.3.0
file_picker: ^8.0.3
url_launcher: ^6.1.5
local_notifier: ^0.1.6
# Server browser
supabase_flutter: ^2.7.0
supabase_flutter: ^2.5.2
uuid: ^3.0.6
dart_ipify: ^1.1.1
# Storage
get_storage: ^2.1.1
get_storage: ^2.0.3
universal_disk_space: ^0.2.3
path: ^1.9.0
path: ^1.8.3
# Translations
intl: any
@@ -67,17 +71,23 @@ dependencies:
# Auto updater
yaml: ^3.1.2
package_info_plus: ^8.0.2
package_info_plus: ^8.0.0
version: ^3.0.2
# Validate profile
email_validator: ^3.0.0
dependency_overrides:
xml: ^6.3.0
http: ^0.13.5
win32: ^3.0.0
ffi: ^2.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter_lints: ^4.0.0
flutter:
uses-material-design: true

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <local_notifier/local_notifier_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
@@ -18,6 +19,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
FlutterAcrylicPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
LocalNotifierPluginRegisterWithRegistrar(

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
bitsdojo_window_windows
flutter_acrylic
local_notifier
screen_retriever

View File

@@ -1,3 +1,6 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
#include <cstdlib>
#include <flutter/dart_project.h>

View File

@@ -121,7 +121,7 @@ bool Win32Window::CreateAndShow(const std::wstring &title,
HWND window = CreateWindow(
window_class,
title.c_str(),
WS_OVERLAPPEDWINDOW,
WS_OVERLAPPED | WS_BORDER | WS_THICKFRAME,
Scale(origin.x, scale_factor),
Scale(origin.y, scale_factor),
Scale(size.width, scale_factor),
@@ -198,9 +198,6 @@ Win32Window::MessageHandler(HWND hwnd,
SetFocus(child_content_);
}
return 0;
case WM_NCCALCSIZE:
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);

View File

@@ -1,13 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
namespace Constants
{
constexpr auto API_URL = L"http://localhost:3551";
constexpr auto ProcessRequest = L"Could not set libcurl options for easy handle, processing HTTP request failed. Increase verbosity for additional information.";
constexpr auto ProcessRequest_C2 = L"STAT_FCurlHttpRequest_ProcessRequest";
constexpr auto URLOffset = L"ProcessRequest failed. URL '%s' is not a valid HTTP request. %p";
constexpr auto Realloc = L"AbilitySystem.Debug.NextTarget";
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#include "Core.h"
void Core::Init()
{
FMemory::_Realloc = Memcury::Scanner::FindStringRef(Constants::Realloc)
.ScanFor({ Memcury::ASM::MNEMONIC::CALL })
.RelativeOffset(1)
.GetAs<decltype(FMemory::_Realloc)>();
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
#include "..\Utilities\memcury.h"
#include "Constants.h"
#include "Unreal\Memory.h"
#include "Unreal\Array.h"
#include "Unreal\String.h"
namespace Core
{
void Init();
}

View File

@@ -1,143 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
#include <functional>
#include "Memory.h"
template <typename T>
class TArray
{
friend class FString;
T* Data;
int32_t NumElements;
int32_t MaxElements;
public:
inline TArray()
{
Data = nullptr;
NumElements = 0;
MaxElements = 0;
};
inline void Free()
{
FMemory::Free(Data);
Data = nullptr;
NumElements = 0;
MaxElements = 0;
}
inline void Reset()
{
Free();
}
inline auto GetData()
{
return Data;
}
inline int GetCount() const
{
return NumElements;
}
inline int Num() const
{
return NumElements;
}
inline auto& Get(const int Index)
{
return Data[Index];
}
inline auto& First()
{
return Get(0);
}
inline auto GetRef(const int Index, int Size = sizeof(T))
{
return (T*)((uint8_t*)Data + (Index * Size));
}
inline T& operator[](int i)
{
return Get(i);
};
inline const T& operator[](int i) const
{
return Get(i);
};
inline bool Remove(const int Index, int Size = sizeof(T))
{
if (Index < NumElements)
{
if (Index != NumElements - 1)
Get(Index) = Get(NumElements - 1);
--NumElements;
return true;
}
return false;
};
inline bool Any(std::function<bool(T)> Func)
{
for (int i = 0; i < NumElements; ++i)
{
if (Func(Get(i)))
return true;
}
return false;
}
inline T Select(std::function<bool(T)> Func)
{
for (int i = 0; i < NumElements; ++i)
{
if (Func(Get(i)))
return Get(i);
}
return NULL;
}
inline void ForEach(std::function<void(T)> Func)
{
for (int i = 0; i < NumElements; ++i)
{
Func(Get(i));
}
}
inline int Count(std::function<bool(T)> Func)
{
int Num = 0;
for (int i = 0; i < NumElements; ++i)
{
if (Func(Get(i)))
Num++;
}
return Num;
}
inline int Find(const T& Item)
{
for (int i = 0; i < NumElements; i++)
{
if (this->operator[](i) == Item)
return i;
}
return -1;
}
};

View File

@@ -1,92 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
#include <Windows.h>
#include <cstdint>
class FMemory
{
public:
static inline void* (*_Realloc)(void*, size_t, int64_t);
static void Free(void* Data)
{
_Realloc(Data, 0, 0);
}
static void* Malloc(size_t Size)
{
return _Realloc(0, Size, 0);
}
static void* Realloc(void* Data, size_t NewSize)
{
return _Realloc(Data, NewSize, 0);
}
static void* Memmove(void* Dest, const void* Src, size_t Count)
{
return memmove(Dest, Src, Count);
}
static int Memcmp(const void* Buf1, const void* Buf2, size_t Count)
{
return memcmp(Buf1, Buf2, Count);
}
static void* Memset(void* Dest, uint8_t Char, size_t Count)
{
return memset(Dest, Char, Count);
}
template< class T >
static void Memset(T& Src, uint8_t ValueToSet)
{
Memset(&Src, ValueToSet, sizeof(T));
}
static void* Memzero(void* Dest, size_t Count)
{
return ZeroMemory(Dest, Count);
}
template <class T>
static void Memzero(T& Src)
{
Memzero(&Src, sizeof(T));
}
static void* Memcpy(void* Dest, const void* Src, size_t Count)
{
return memcpy(Dest, Src, Count);
}
template <class T>
static void Memcpy(T& Dest, const T& Src)
{
Memcpy(&Dest, &Src, sizeof(T));
}
static void* Calloc(size_t NumElements, size_t ElementSize)
{
auto TotalSize = NumElements * ElementSize;
auto Data = FMemory::Malloc(TotalSize);
if (!Data)
return NULL;
FMemory::Memzero(Data, TotalSize);
return Data;
}
static char* Strdup(const char* Str)
{
auto StrLen = strlen(Str) + 1;
auto StrDup = (char*)FMemory::Malloc(StrLen);
FMemory::Memcpy(StrDup, Str, StrLen);
return StrDup;
}
};

View File

@@ -1,54 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
#include "Array.h"
#include "Memory.h"
class FString : private TArray<wchar_t>
{
public:
inline FString()
{
Data = nullptr;
NumElements = 0;
MaxElements = 0;
}
inline FString(const char* Other)
{
if (Other)
{
auto NumCharacters = (int)std::strlen(Other);
MaxElements = NumElements = NumCharacters + 1;
Data = static_cast<wchar_t*>(FMemory::Malloc(NumElements * sizeof(wchar_t)));
size_t ConvertedChars = 0;
mbstowcs_s(&ConvertedChars, Data, NumElements, Other, _TRUNCATE);
}
else
{
MaxElements = NumElements = 0;
Data = nullptr;
}
};
inline FString(const wchar_t* Other)
{
MaxElements = NumElements = *Other ? (int)std::wcslen(Other) + 1 : 0;
if (NumElements && Other)
{
Data = static_cast<wchar_t*>(FMemory::Malloc(NumElements * 2));
memcpy_s(Data, NumElements * 2, Other, NumElements * 2);
}
};
inline auto c_str()
{
return Data;
}
};

View File

@@ -1,21 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#include "framework.h"
static void Main()
{
Sleep(7500);
Core::Init();
Sinum::Init();
}
bool DllMain(HMODULE hModule, DWORD dwReason, void* lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
Windows::Thread::Create(Main);
}
return true;
}

View File

@@ -1,5 +0,0 @@
# Sinum Windows
https://github.com/projectnovafn/Sinum/tree/main/Windows
Modified to point to http://localhost:3551

View File

@@ -1,22 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34031.279
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Sinum", "Sinum.vcxproj", "{E7291B57-1B5B-497C-9C2E-C78A556A06CF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E7291B57-1B5B-497C-9C2E-C78A556A06CF}.Release|x64.ActiveCfg = Release|x64
{E7291B57-1B5B-497C-9C2E-C78A556A06CF}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7975A2E1-0078-4AF8-AB10-0A71112857A5}
EndGlobalSection
EndGlobal

View File

@@ -1,162 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{e7291b57-1b5b-497c-9c2e-c78a556a06cf}</ProjectGuid>
<RootNamespace>Sinum</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;SINUM_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;SINUM_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;SINUM_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;SINUM_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<Optimization>MaxSpeed</Optimization>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Core\Constants.h" />
<ClInclude Include="Core\Unreal\Array.h" />
<ClInclude Include="Core\Core.h" />
<ClInclude Include="Core\Unreal\Memory.h" />
<ClInclude Include="Core\Unreal\String.h" />
<ClInclude Include="framework.h" />
<ClInclude Include="Sinum\Sinum.h" />
<ClInclude Include="Utilities\memcury.h" />
<ClInclude Include="Utilities\Windows.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Core\Core.cpp" />
<ClCompile Include="Main.cpp" />
<ClCompile Include="Sinum\Sinum.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="framework.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Utilities\Windows.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Core\Unreal\Array.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Core\Constants.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Core\Unreal\String.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Core\Unreal\Memory.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Core\Core.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Sinum\Sinum.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Utilities\memcury.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Sinum\Sinum.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Core\Core.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

View File

@@ -1,40 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#include "Sinum.h"
bool Sinum::ProcessRequestHook(FCurlHttpRequest* Request)
{
std::wstring URL(Request->GetURL().c_str());
size_t PathIndex = URL.find(L"ol.epicgames.com");
if (PathIndex != std::wstring::npos)
{
auto Path = URL.substr(PathIndex + 16);
auto NewURL = Constants::API_URL + Path;
Request->SetURL(NewURL.c_str());
}
return _ProcessRequest(Request);
}
void Sinum::Init()
{
auto StringRef = Memcury::Scanner::FindStringRef(Constants::ProcessRequest);
if (StringRef.IsValid())
{
_ProcessRequest = StringRef
.ScanFor({ 0x48, 0x81, 0xEC }, false)
.ScanFor({ 0x40 }, false)
.GetAs<decltype(_ProcessRequest)>();
}
else
{
_ProcessRequest = Memcury::Scanner::FindStringRef(Constants::ProcessRequest_C2)
.ScanFor({ 0x4C, 0x8B, 0xDC }, false)
.GetAs<decltype(_ProcessRequest)>();
}
*Memcury::Scanner::FindPointerRef(_ProcessRequest)
.GetAs<void**>() = ProcessRequestHook;
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
#include "../framework.h"
class FCurlHttpRequest
{
private:
void** VTable;
public:
FString GetURL()
{
FString Result;
return ((FString& (*)(FCurlHttpRequest*, FString&))(*VTable))(this, Result);
}
void SetURL(FString URL)
{
((void (*)(FCurlHttpRequest*, FString&))(VTable[10]))(this, URL);
}
};
namespace Sinum
{
static bool (*_ProcessRequest)(FCurlHttpRequest*);
static bool ProcessRequestHook(FCurlHttpRequest* Request);
void Init();
}

View File

@@ -1,15 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
#include "../framework.h"
namespace Windows
{
namespace Thread
{
static HANDLE Create(void* Routine, void* Param = NULL)
{
return CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Routine, Param, 0, NULL);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +0,0 @@
// Copyright (c) 2024 Project Nova LLC
#pragma once
#include <Windows.h>
#include "Utilities\memcury.h"
#include "Utilities\Windows.h"
#include "Core\Core.h"
#include "Sinum\Sinum.h"