launch game

This commit is contained in:
xkeyC 2023-10-28 18:19:18 +08:00
parent 0640761299
commit e465bc913d
9 changed files with 395 additions and 46 deletions

View File

@ -1,5 +1,3 @@
/// https://github.com/CxJuice/Uex_Chinese_Translate
/// ------- WebLocalization Script --------------
let SCLocalizationReplaceLocalesMap = {};
let enable_webview_localization_capture = false;
@ -220,7 +218,7 @@ InitWebLocalization();
/// ----- Login Script ----
async function getRSILauncherToken() {
async function getRSILauncherToken(channelId) {
// check login
let r = await fetch("api/launcher/v3/account/check", {
method: 'POST', headers: {
@ -253,12 +251,12 @@ async function getRSILauncherToken() {
});
if (tokenR.status !== 200) return;
let TokenData = (await tokenR.json())["data"];
let TokenData = (await tokenR.json())["data"]["token"];
console.log(TokenData);
// get release Data
let releaseFormData = new FormData();
releaseFormData.append("channelId", "LIVE");
releaseFormData.append("channelId", channelId);
releaseFormData.append("claims", claimsData);
releaseFormData.append("gameId", "SC");
releaseFormData.append("platformId", "prod");
@ -271,6 +269,9 @@ async function getRSILauncherToken() {
if (releaseR.status !== 200) return;
let releaseDataJson = (await releaseR.json())['data'];
console.log(releaseDataJson);
// get user avatar
let $avatarElement = $(".c-account-sidebar__profile-metas-avatar");
let avatarUrl = $avatarElement.css("background-image");
// post message
window.chrome.webview.postMessage({
@ -278,7 +279,8 @@ async function getRSILauncherToken() {
'webToken': $.cookie('Rsi-Token'),
'claims': claimsData,
'authToken': TokenData,
'releaseInfo': releaseDataJson
'releaseInfo': releaseDataJson,
"avatar": avatarUrl
}
});
}

View File

@ -237,10 +237,10 @@ class HomeUI extends BaseUI<HomeUIModel> {
child: Center(
child: Icon(
FontAwesomeIcons.solidCircle,
color:
model.isRSIServerStatusOK(item)
? Colors.green
: Colors.red,
color: model
.isRSIServerStatusOK(item)
? Colors.green
: Colors.red,
size: 12,
),
),
@ -296,14 +296,14 @@ class HomeUI extends BaseUI<HomeUIModel> {
const SizedBox(width: 12),
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: model.isRsiLauncherStarting
? makeLoading(context, width: 28)
: Button(
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(FluentIcons.play),
),
onPressed: () => model.launchRSI()),
child: Button(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(model.isCurGameRunning
? FluentIcons.stop_solid
: FluentIcons.play),
),
onPressed: () => model.launchRSI()),
),
const SizedBox(width: 12),
Button(

View File

@ -13,6 +13,8 @@ import 'package:starcitizen_doctor/data/app_placard_data.dart';
import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui.dart';
import 'package:starcitizen_doctor/ui/home/dialogs/md_content_dialog_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/login/login_dialog_ui.dart';
import 'package:starcitizen_doctor/ui/home/login/login_dialog_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/performance/performance_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview_localization_capture_ui_model.dart';
@ -36,6 +38,10 @@ class HomeUIModel extends BaseUIModel {
bool isFixing = false;
String isFixingString = "";
final Map<String, bool> _isGameRunning = {};
bool get isCurGameRunning => _isGameRunning[scInstalledPath] ?? false;
set lastScreenInfo(String info) {
_lastScreenInfo = info;
notifyListeners();
@ -58,8 +64,6 @@ class HomeUIModel extends BaseUIModel {
"Arena Commander": "竞技场指挥官"
};
bool isRsiLauncherStarting = false;
@override
Future loadData() async {
if (AppConf.networkVersionData == null) return;
@ -110,7 +114,7 @@ class HomeUIModel extends BaseUIModel {
return;
}
scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
withVersion: ["LIVE", "PTU", "EPTU"], checkExists: true);
withVersion: ["LIVE", "PTU", "EVO"], checkExists: true);
if (scInstallPaths.isNotEmpty) {
scInstalledPath = scInstallPaths.first;
}
@ -402,8 +406,8 @@ class HomeUIModel extends BaseUIModel {
context!,
"星际公民官网汉化",
const Text(
"\n\n\n本插功能件仅供大致浏览使用,不对任何有关本功能产生的问题负责!在涉及账号操作前请注意确认网站的原本内容!"
"\n\n\n使用此功能登录账号时请确保您的 StarCitizenDoctor 是从可信任的来源下载。",
"本插功能件仅供大致浏览使用,不对任何有关本功能产生的问题负责!在涉及账号操作前请注意确认网站的原本内容!"
"\n\n\n使用此功能登录账号时请确保您的 星际公民盒子 是从可信任的来源下载。",
style: TextStyle(fontSize: 16),
),
constraints: BoxConstraints(
@ -460,25 +464,39 @@ class HomeUIModel extends BaseUIModel {
}
launchRSI() async {
isRsiLauncherStarting = true;
notifyListeners();
// final rsiLauncherInstalledPath = await SystemHelper.getRSILauncherPath();
// if (rsiLauncherInstalledPath.isEmpty) {
// isRsiLauncherStarting = false;
// notifyListeners();
// showToast(context!, "未找到 RSI 启动器目录");
// return;
// }
// SystemHelper.checkAndLaunchRSILauncher(rsiLauncherInstalledPath);
goWebView("登录 RSI 账户", "https://robertsspaceindustries.com/connect",
loginMode: true, rsiLoginCallback: (data, ok) {
dPrint("======rsiLoginCallback=== $ok =====\n$data}");
isRsiLauncherStarting = false;
notifyListeners();
}, useLocalization: true);
if (scInstalledPath == "not_install") {
showToast(context!, "该功能需要一个有效的安装位置");
return;
}
if (isCurGameRunning) {
await Process.run("powershell.exe", ["ps \"StarCitizen\" | kill"]);
return;
}
showDialog(
context: context!,
dismissWithEsc: false,
builder: (context) {
return BaseUIContainer(
uiCreate: () => LoginDialog(),
modelCreate: () => LoginDialogModel(scInstalledPath, this));
});
}
bool isRSIServerStatusOK(Map map) {
return (map["status"] == "ok" || map["status"] == "operational");
}
doLaunchGame(String launchExe, List<String> args, String installPath) async {
_isGameRunning[installPath] = true;
notifyListeners();
try {
await Process.run(launchExe, args);
final launchFile = File("$installPath\\loginData.json");
if (await launchFile.exists()) {
await launchFile.delete();
}
} catch (_) {}
_isGameRunning[installPath] = false;
notifyListeners();
}
}

View File

@ -0,0 +1,106 @@
import 'package:starcitizen_doctor/base/ui.dart';
import 'package:starcitizen_doctor/widgets/cache_image.dart';
import 'login_dialog_ui_model.dart';
class LoginDialog extends BaseUI<LoginDialogModel> {
@override
Widget? buildBody(BuildContext context, LoginDialogModel model) {
return ContentDialog(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .56,
),
title: (model.loginStatus == 2) ? null : const Text("一键启动"),
content: AnimatedSize(
duration: const Duration(milliseconds: 230),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Row(),
if (model.loginStatus == 0) ...[
const Center(
child: Column(
children: [
Text("登录中..."),
SizedBox(height: 12),
ProgressRing()
],
),
),
] else if (model.loginStatus == 1) ...[
Text("请输入RSI账户 [${model.nickname}] 的邮箱,以保存登录状态(输入错误会导致无法进入游戏!)"),
const SizedBox(height: 12),
TextFormBox(
controller: model.emailCtrl,
),
const SizedBox(height: 6),
Text(
"*该操作同一账号只需执行一次,输入错误请在盒子设置中清理,切换账号请在汉化浏览器中操作。",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.6),
),
)
] else if (model.loginStatus == 2) ...[
Center(
child: Column(
children: [
const SizedBox(height: 12),
const Text(
"欢迎回来!",
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 24),
if (model.avatarUrl != null)
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: CacheNetImage(
url: model.avatarUrl!,
width: 128,
height: 128,
fit: BoxFit.fill,
),
),
const SizedBox(height: 12),
Text(
model.nickname,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
const Text("正在为您启动游戏..."),
const SizedBox(height: 12),
const ProgressRing(),
],
),
)
]
],
),
),
actions: [
if (model.loginStatus == 1) ...[
Button(
child: const Padding(
padding: EdgeInsets.all(4),
child: Text("取消"),
),
onPressed: () {
Navigator.pop(context);
}),
const SizedBox(width: 80),
FilledButton(
child: const Padding(
padding: EdgeInsets.all(4),
child: Text("保存"),
),
onPressed: () => model.onSaveEmail()),
],
],
);
}
@override
String getUITitle(BuildContext context, LoginDialogModel model) => "";
}

View File

@ -0,0 +1,174 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:hive/hive.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'package:starcitizen_doctor/base/ui_model.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/webview/webview.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:uuid/uuid.dart';
class LoginDialogModel extends BaseUIModel {
int loginStatus = 0;
String nickname = "";
String? avatarUrl;
String? authToken;
String? webToken;
Map? releaseInfo;
final String installPath;
final HomeUIModel homeUIModel;
TextEditingController emailCtrl = TextEditingController();
LoginDialogModel(this.installPath, this.homeUIModel);
@override
void initModel() {
_launchWebLogin();
super.initModel();
}
void _launchWebLogin() {
goWebView("登录 RSI 账户", "https://robertsspaceindustries.com/connect",
loginMode: true, rsiLoginCallback: (message, ok) async {
dPrint(
"======rsiLoginCallback=== $ok ===== data==\n${json.encode(message)}");
if (message == null || !ok) {
Navigator.pop(context!);
return;
}
final emailBox = await Hive.openBox("quick_login_email");
final data = message["data"];
authToken = data["authToken"];
webToken = data["webToken"];
releaseInfo = data["releaseInfo"];
avatarUrl = data["avatar"]
?.toString()
.replaceAll("url(\"", "")
.replaceAll("\")", "");
Map<String, dynamic> payload = Jwt.parseJwt(authToken!);
nickname = payload["nickname"] ?? "";
if (emailBox.get(nickname, defaultValue: "") == "") {
loginStatus = 1;
notifyListeners();
} else {
emailCtrl.text = emailBox.get(nickname, defaultValue: "");
_readyForLaunch();
}
}, useLocalization: true);
}
goWebView(String title, String url,
{bool useLocalization = false,
bool loginMode = false,
RsiLoginCallback? rsiLoginCallback}) async {
if (useLocalization) {
const tipVersion = 2;
final box = await Hive.openBox("app_conf");
final skip = await box.get("skip_web_login_version", defaultValue: 0);
if (skip != tipVersion) {
final ok = await showConfirmDialogs(
context!,
"星际公民盒子一键启动",
const Text(
"本功能可以帮您更加便利的启动游戏。\n\n为确保账户安全 ,本功能使用汉化浏览器保留登录状态,且不会保存您的密码信息,与 RSI 启动器行为一致。"
"\n\n使用此功能登录账号时请确保您的 星际公民盒子 是从可信任的来源下载。",
style: TextStyle(fontSize: 16),
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context!).size.width * .6));
if (!ok) {
if (loginMode) {
rsiLoginCallback?.call(null, false);
}
return;
}
await box.put("skip_web_login_version", tipVersion);
}
}
if (!await WebviewWindow.isWebviewAvailable()) {
await showToast(context!, "需要安装 WebView2 Runtime");
await launchUrlString(
"https://developer.microsoft.com/en-us/microsoft-edge/webview2/");
Navigator.pop(context!);
return;
}
final webViewModel = WebViewModel(context!,
loginMode: loginMode,
loginCallback: rsiLoginCallback,
loginChannel: getChannelID());
if (useLocalization) {
try {
await webViewModel.initLocalization();
} catch (_) {}
}
await webViewModel.initWebView(
title: title,
);
await webViewModel.launch(url);
notifyListeners();
}
onSaveEmail() async {
final RegExp emailRegex = RegExp(r'^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$');
if (!emailRegex.hasMatch(emailCtrl.text.trim())) {
showToast(context!, "邮箱输入有误!");
return;
}
final emailBox = await Hive.openBox("quick_login_email");
await emailBox.put(nickname, emailCtrl.text.trim());
_readyForLaunch();
notifyListeners();
}
Future<void> _readyForLaunch() async {
loginStatus = 2;
notifyListeners();
final launchData = {
"username": emailCtrl.text.trim(),
"token": webToken,
"auth_token": authToken,
"star_network": {
"services_endpoint": releaseInfo?["servicesEndpoint"],
"hostname": releaseInfo?["universeHost"],
"port": releaseInfo?["universePort"],
},
"TMid": const Uuid().v4(),
};
final executable = releaseInfo?["executable"];
final launchOptions = releaseInfo?["launchOptions"];
dPrint("----------launch data ====== -----------\n$launchData");
dPrint(
"----------executable data ====== -----------\n$installPath\\$executable $launchOptions");
final launchFile = File("$installPath\\loginData.json");
if (await launchFile.exists()) {
await launchFile.delete();
}
await launchFile.create();
await launchFile.writeAsString(json.encode(launchData));
notifyListeners();
await Future.delayed(const Duration(seconds: 1));
homeUIModel.doLaunchGame(
'$installPath\\$executable',
["-no_login_dialog", ...launchOptions.toString().split(" ")],
installPath);
await Future.delayed(const Duration(seconds: 3));
Navigator.pop(context!);
}
String getChannelID() {
if (installPath.endsWith("\\LIVE")) {
return "LIVE";
} else if (installPath.endsWith("\\PTU")) {
return "PTU";
} else if (installPath.endsWith("\\EVO")) {
return "EVO";
}
return "LIVE";
}
}

View File

@ -23,7 +23,8 @@ class WebViewModel {
bool get isClosed => _isClosed;
WebViewModel(this.context, {this.loginMode = false, this.loginCallback});
WebViewModel(this.context,
{this.loginMode = false, this.loginCallback, this.loginChannel = "LIVE"});
String url = "";
bool canGoBack = false;
@ -39,6 +40,7 @@ class WebViewModel {
Map<String, String>? get curReplaceWords => _curReplaceWords;
final bool loginMode;
final String loginChannel;
bool _loginModeSuccess = false;
@ -56,7 +58,6 @@ class WebViewModel {
if (loginMode) {
await webview.setWebviewWindowVisibility(false);
}
// webview.openDevToolsWindow();
webview.isNavigating.addListener(() async {
if (!webview.isNavigating.value && localizationResource.isNotEmpty) {
@ -117,10 +118,14 @@ class WebViewModel {
await Future.delayed(const Duration(milliseconds: 100));
await webview.evaluateJavaScript(
"WebLocalizationUpdateReplaceWords(${json.encode(replaceWords)},$enableCapture)");
/// loginMode
if (loginMode) {
dPrint("--- do rsi login ---");
dPrint(
"--- do rsi login ---\n run === getRSILauncherToken(\"$loginChannel\");");
await Future.delayed(const Duration(milliseconds: 200));
webview.evaluateJavaScript("getRSILauncherToken();");
webview.evaluateJavaScript(
"getRSILauncherToken(\"$loginChannel\");");
}
} else if (uri.host.contains("www.erkul.games") ||
uri.host.contains("uexcorp.space") ||
@ -163,8 +168,7 @@ class WebViewModel {
}
initLocalization() async {
localizationScript =
await rootBundle.loadString('assets/localization_web_script.js');
localizationScript = await rootBundle.loadString('assets/web_script.js');
/// https://github.com/CxJuice/Uex_Chinese_Translate
// get versions
@ -172,6 +176,7 @@ class WebViewModel {
final v = AppWebLocalizationVersionsData.fromJson(
await _getJson("$hostUrl/versions.json"));
dPrint("AppWebLocalizationVersionsData === ${v.toJson()}");
localizationResource["zh-CN"] = await _getJson("$hostUrl/zh-CN-rsi.json",

View File

@ -183,7 +183,7 @@ class ToolsUIModel extends BaseUIModel {
return;
}
scInstallPaths = await SCLoggerHelper.getGameInstallPath(listData,
checkExists: false, withVersion: ["LIVE", "PTU", "EPTU"]);
checkExists: false, withVersion: ["LIVE", "PTU", "EVO"]);
if (scInstallPaths.isNotEmpty) {
scInstalledPath = scInstallPaths.first;
}

View File

@ -0,0 +1,41 @@
import 'package:extended_image/extended_image.dart';
import 'package:fluent_ui/fluent_ui.dart';
class CacheNetImage extends StatelessWidget {
final String url;
final double? width;
final double? height;
final BoxFit? fit;
const CacheNetImage(
{super.key, required this.url, this.width, this.height, this.fit});
@override
Widget build(BuildContext context) {
return ExtendedImage.network(
url,
width: width,
height: height,
fit: fit,
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading:
return const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
ProgressRing(),
],
),
),
);
case LoadState.failed:
return const Text("Loading Image error");
case LoadState.completed:
return null;
}
},
);
}
}

View File

@ -49,6 +49,9 @@ dependencies:
desktop_webview_window: ^0.2.3
flutter_svg: ^2.0.7
archive: ^3.4.4
jwt_decode: ^0.3.1
html: ^0.15.4
uuid: ^4.1.0
dev_dependencies:
flutter_test: