mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-13 11:40:27 +00:00
feat: 增加基础的游戏进程监听
This commit is contained in:
parent
f6340337db
commit
b65187d4f0
495
lib/common/helper/game_log_analyzer.dart
Normal file
495
lib/common/helper/game_log_analyzer.dart
Normal file
@ -0,0 +1,495 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||||
import 'package:starcitizen_doctor/generated/l10n.dart';
|
||||
|
||||
/// 日志分析结果数据类
|
||||
class LogAnalyzeLineData {
|
||||
final String type;
|
||||
final String title;
|
||||
final String? data;
|
||||
final String? dateTime;
|
||||
final String? tag; // 统计标签,用于定位日志(如 "game_start"),不依赖本地化
|
||||
|
||||
// 格式化后的字段
|
||||
final String? victimId; // 受害者ID (actor_death)
|
||||
final String? killerId; // 击杀者ID (actor_death)
|
||||
final String? location; // 位置信息 (request_location_inventory)
|
||||
final String? playerName; // 玩家名称 (player_login)
|
||||
|
||||
const LogAnalyzeLineData({
|
||||
required this.type,
|
||||
required this.title,
|
||||
this.data,
|
||||
this.dateTime,
|
||||
this.tag,
|
||||
this.victimId,
|
||||
this.killerId,
|
||||
this.location,
|
||||
this.playerName,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志分析统计数据
|
||||
class LogAnalyzeStatistics {
|
||||
final String playerName;
|
||||
final int killCount;
|
||||
final int deathCount;
|
||||
final int selfKillCount;
|
||||
final int vehicleDestructionCount;
|
||||
final int vehicleDestructionCountHard;
|
||||
final DateTime? gameStartTime;
|
||||
final int gameCrashLineNumber;
|
||||
final String? latestLocation; // 最新位置信息(全量查找)
|
||||
|
||||
const LogAnalyzeStatistics({
|
||||
required this.playerName,
|
||||
required this.killCount,
|
||||
required this.deathCount,
|
||||
required this.selfKillCount,
|
||||
required this.vehicleDestructionCount,
|
||||
required this.vehicleDestructionCountHard,
|
||||
this.gameStartTime,
|
||||
required this.gameCrashLineNumber,
|
||||
this.latestLocation,
|
||||
});
|
||||
}
|
||||
|
||||
/// 游戏日志分析器
|
||||
class GameLogAnalyzer {
|
||||
static const String unknownValue = "<Unknown>";
|
||||
|
||||
// 正则表达式定义
|
||||
static final _baseRegExp = RegExp(r'\[Notice\]\s+<([^>]+)>');
|
||||
static final _gameLoadingRegExp = RegExp(
|
||||
r'<[^>]+>\s+Loading screen for\s+(\w+)\s+:\s+SC_Frontend closed after\s+(\d+\.\d+)\s+seconds',
|
||||
);
|
||||
static final _logDateTimeRegExp = RegExp(r'<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)>');
|
||||
static final DateFormat _dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss:SSS');
|
||||
|
||||
// 致命碰撞解析
|
||||
static final _fatalCollisionPatterns = {
|
||||
'zone': RegExp(r'\[Part:[^\]]*?Zone:\s*([^,\]]+)'),
|
||||
'player_pilot': RegExp(r'PlayerPilot:\s*(\d)'),
|
||||
'hit_entity': RegExp(r'hitting entity:\s*(\w+)'),
|
||||
'hit_entity_vehicle': RegExp(r'hitting entity:[^\[]*\[Zone:\s*([^\s-]+)'),
|
||||
'distance': RegExp(r'Distance:\s*([\d.]+)'),
|
||||
};
|
||||
|
||||
// 载具损毁解析
|
||||
static final _vehicleDestructionPattern = RegExp(
|
||||
r"Vehicle\s+'([^']+)'.*?" // 载具型号
|
||||
r"in zone\s+'([^']+)'.*?" // Zone
|
||||
r"destroy level \d+ to (\d+).*?" // 损毁等级
|
||||
r"caused by\s+'([^']+)'", // 责任方
|
||||
);
|
||||
|
||||
// 角色死亡解析
|
||||
static final _actorDeathPattern = RegExp(
|
||||
r"CActor::Kill: '([^']+)'.*?" // 受害者ID
|
||||
r"in zone '([^']+)'.*?" // 死亡位置区域
|
||||
r"killed by '([^']+)'.*?" // 击杀者ID
|
||||
r"with damage type '([^']+)'", // 伤害类型
|
||||
);
|
||||
|
||||
// 角色名称解析
|
||||
static final _characterNamePattern = RegExp(r"name\s+([^-]+)");
|
||||
|
||||
// 本地库存请求解析
|
||||
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
|
||||
|
||||
/// 分析整个日志文件
|
||||
///
|
||||
/// [logFile] 日志文件
|
||||
/// [startTime] 开始时间,如果提供则只统计此时间之后的数据
|
||||
/// 返回日志分析结果列表和统计数据
|
||||
static Future<(List<LogAnalyzeLineData>, LogAnalyzeStatistics)> analyzeLogFile(
|
||||
File logFile, {
|
||||
DateTime? startTime,
|
||||
}) async {
|
||||
if (!(await logFile.exists())) {
|
||||
return (
|
||||
[LogAnalyzeLineData(type: "error", title: S.current.log_analyzer_no_log_file)],
|
||||
LogAnalyzeStatistics(
|
||||
playerName: "",
|
||||
killCount: 0,
|
||||
deathCount: 0,
|
||||
selfKillCount: 0,
|
||||
vehicleDestructionCount: 0,
|
||||
vehicleDestructionCountHard: 0,
|
||||
gameCrashLineNumber: -1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final logLines = utf8.decode((await logFile.readAsBytes()), allowMalformed: true).split("\n");
|
||||
return _analyzeLogLines(logLines, startTime: startTime);
|
||||
}
|
||||
|
||||
/// 分析日志行列表
|
||||
///
|
||||
/// [logLines] 日志行列表
|
||||
/// [startTime] 开始时间,如果提供则只影响计数统计,不影响 gameStartTime 和位置的全量查找
|
||||
/// 返回日志分析结果列表和统计数据
|
||||
static (List<LogAnalyzeLineData>, LogAnalyzeStatistics) _analyzeLogLines(
|
||||
List<String> logLines, {
|
||||
DateTime? startTime,
|
||||
}) {
|
||||
final results = <LogAnalyzeLineData>[];
|
||||
String playerName = "";
|
||||
int killCount = 0;
|
||||
int deathCount = 0;
|
||||
int selfKillCount = 0;
|
||||
int vehicleDestructionCount = 0;
|
||||
int vehicleDestructionCountHard = 0;
|
||||
DateTime? gameStartTime; // 全量查找,不受 startTime 影响
|
||||
String? latestLocation; // 全量查找最新位置
|
||||
int gameCrashLineNumber = -1;
|
||||
bool shouldCount = startTime == null; // 只影响计数
|
||||
|
||||
for (var i = 0; i < logLines.length; i++) {
|
||||
final line = logLines[i];
|
||||
if (line.isEmpty) continue;
|
||||
|
||||
// 如果设置了 startTime,检查当前行时间
|
||||
if (startTime != null && !shouldCount) {
|
||||
final lineTime = _getLogLineDateTime(line);
|
||||
if (lineTime != null && lineTime.isAfter(startTime)) {
|
||||
shouldCount = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理游戏开始(全量查找第一次出现)
|
||||
if (gameStartTime == null) {
|
||||
gameStartTime = _getLogLineDateTime(line);
|
||||
if (gameStartTime != null) {
|
||||
results.add(
|
||||
LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: S.current.log_analyzer_game_start,
|
||||
tag: "game_start", // 使用 tag 标识,不依赖本地化
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏加载时间
|
||||
final gameLoading = _parseGameLoading(line);
|
||||
if (gameLoading != null) {
|
||||
results.add(
|
||||
LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: S.current.log_analyzer_game_loading,
|
||||
data: S.current.log_analyzer_mode_loading_time(gameLoading.$1, gameLoading.$2),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 基础事件解析
|
||||
final baseEvent = _parseBaseEvent(line);
|
||||
if (baseEvent != null) {
|
||||
LogAnalyzeLineData? data;
|
||||
switch (baseEvent) {
|
||||
case "AccountLoginCharacterStatus_Character":
|
||||
data = _parseCharacterName(line);
|
||||
if (data != null && data.playerName != null) {
|
||||
playerName = data.playerName!; // 全量更新玩家名称
|
||||
}
|
||||
break;
|
||||
case "FatalCollision":
|
||||
data = _parseFatalCollision(line);
|
||||
break;
|
||||
case "Vehicle Destruction":
|
||||
data = _parseVehicleDestruction(line, playerName, shouldCount, (isHard) {
|
||||
if (isHard) {
|
||||
vehicleDestructionCountHard++;
|
||||
} else {
|
||||
vehicleDestructionCount++;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "Actor Death":
|
||||
data = _parseActorDeath(line, playerName, shouldCount, (isKill, isDeath, isSelfKill) {
|
||||
if (isSelfKill) {
|
||||
selfKillCount++;
|
||||
} else {
|
||||
if (isKill) killCount++;
|
||||
if (isDeath) deathCount++;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "RequestLocationInventory":
|
||||
data = _parseRequestLocationInventory(line);
|
||||
if (data != null && data.location != null) {
|
||||
latestLocation = data.location; // 全量更新最新位置
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (data != null) {
|
||||
results.add(data);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏关闭
|
||||
if (line.contains("[CIG] CCIGBroker::FastShutdown")) {
|
||||
results.add(
|
||||
LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: S.current.log_analyzer_game_close,
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 游戏崩溃
|
||||
if (line.contains("Cloud Imperium Games public crash handler")) {
|
||||
gameCrashLineNumber = i;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理崩溃信息
|
||||
if (gameCrashLineNumber > 0) {
|
||||
final lastLineDateTime = gameStartTime != null
|
||||
? _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20")))
|
||||
: null;
|
||||
final crashInfo = logLines.sublist(gameCrashLineNumber);
|
||||
final info = SCLoggerHelper.getGameRunningLogInfo(crashInfo);
|
||||
crashInfo.add(S.current.log_analyzer_one_click_diagnosis_header);
|
||||
if (info != null) {
|
||||
crashInfo.add(info.key);
|
||||
if (info.value.isNotEmpty) {
|
||||
crashInfo.add(S.current.log_analyzer_details_info(info.value));
|
||||
}
|
||||
} else {
|
||||
crashInfo.add(S.current.log_analyzer_no_crash_detected);
|
||||
}
|
||||
results.add(
|
||||
LogAnalyzeLineData(
|
||||
type: "game_crash",
|
||||
title: S.current.log_analyzer_game_crash,
|
||||
data: crashInfo.join("\n"),
|
||||
dateTime: lastLineDateTime != null ? _dateTimeFormatter.format(lastLineDateTime) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 添加统计信息
|
||||
if (killCount > 0 || deathCount > 0) {
|
||||
results.add(
|
||||
LogAnalyzeLineData(
|
||||
type: "statistics",
|
||||
title: S.current.log_analyzer_kill_summary,
|
||||
data: S.current.log_analyzer_kill_death_suicide_count(
|
||||
killCount,
|
||||
deathCount,
|
||||
selfKillCount,
|
||||
vehicleDestructionCount,
|
||||
vehicleDestructionCountHard,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 统计游戏时长
|
||||
if (gameStartTime != null) {
|
||||
final lastLineDateTime = _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20"), orElse: () => ""));
|
||||
if (lastLineDateTime != null) {
|
||||
final duration = lastLineDateTime.difference(gameStartTime);
|
||||
results.add(
|
||||
LogAnalyzeLineData(
|
||||
type: "statistics",
|
||||
title: S.current.log_analyzer_play_time,
|
||||
data: S.current.log_analyzer_play_time_format(
|
||||
duration.inHours,
|
||||
duration.inMinutes.remainder(60),
|
||||
duration.inSeconds.remainder(60),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final statistics = LogAnalyzeStatistics(
|
||||
playerName: playerName,
|
||||
killCount: killCount,
|
||||
deathCount: deathCount,
|
||||
selfKillCount: selfKillCount,
|
||||
vehicleDestructionCount: vehicleDestructionCount,
|
||||
vehicleDestructionCountHard: vehicleDestructionCountHard,
|
||||
gameStartTime: gameStartTime,
|
||||
gameCrashLineNumber: gameCrashLineNumber,
|
||||
latestLocation: latestLocation,
|
||||
);
|
||||
|
||||
return (results, statistics);
|
||||
}
|
||||
|
||||
// ==================== 解析辅助方法 ====================
|
||||
|
||||
static String? _parseBaseEvent(String line) {
|
||||
final match = _baseRegExp.firstMatch(line);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
static (String, String)? _parseGameLoading(String line) {
|
||||
final match = _gameLoadingRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
return (match.group(1) ?? "-", match.group(2) ?? "-");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static DateTime? _getLogLineDateTime(String line) {
|
||||
final match = _logDateTimeRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
final dateTimeString = match.group(1);
|
||||
if (dateTimeString != null) {
|
||||
return DateTime.parse(dateTimeString).toLocal();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _getLogLineDateTimeString(String line) {
|
||||
final dateTime = _getLogLineDateTime(line);
|
||||
if (dateTime != null) {
|
||||
return _dateTimeFormatter.format(dateTime);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _safeExtract(RegExp pattern, String line) => pattern.firstMatch(line)?.group(1)?.trim();
|
||||
|
||||
static LogAnalyzeLineData? _parseFatalCollision(String line) {
|
||||
final zone = _safeExtract(_fatalCollisionPatterns['zone']!, line) ?? unknownValue;
|
||||
final playerPilot = (_safeExtract(_fatalCollisionPatterns['player_pilot']!, line) ?? '0') == '1';
|
||||
final hitEntity = _safeExtract(_fatalCollisionPatterns['hit_entity']!, line) ?? unknownValue;
|
||||
final hitEntityVehicle = _safeExtract(_fatalCollisionPatterns['hit_entity_vehicle']!, line) ?? unknownValue;
|
||||
final distance = double.tryParse(_safeExtract(_fatalCollisionPatterns['distance']!, line) ?? '') ?? 0.0;
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "fatal_collision",
|
||||
title: S.current.log_analyzer_filter_fatal_collision,
|
||||
data: S.current.log_analyzer_collision_details(
|
||||
zone,
|
||||
playerPilot ? '✅' : '❌',
|
||||
hitEntity,
|
||||
hitEntityVehicle,
|
||||
distance.toStringAsFixed(2),
|
||||
),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
static LogAnalyzeLineData? _parseVehicleDestruction(
|
||||
String line,
|
||||
String playerName,
|
||||
bool shouldCount,
|
||||
void Function(bool isHard) onDestruction,
|
||||
) {
|
||||
final match = _vehicleDestructionPattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final vehicleModel = match.group(1) ?? unknownValue;
|
||||
final zone = match.group(2) ?? unknownValue;
|
||||
final destructionLevel = int.tryParse(match.group(3) ?? '') ?? 0;
|
||||
final causedBy = match.group(4) ?? unknownValue;
|
||||
|
||||
final destructionLevelMap = {1: S.current.log_analyzer_soft_death, 2: S.current.log_analyzer_disintegration};
|
||||
|
||||
if (shouldCount && causedBy.trim() == playerName) {
|
||||
onDestruction(destructionLevel == 2);
|
||||
}
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "vehicle_destruction",
|
||||
title: S.current.log_analyzer_filter_vehicle_damaged,
|
||||
data: S.current.log_analyzer_vehicle_damage_details(
|
||||
vehicleModel,
|
||||
zone,
|
||||
destructionLevel.toString(),
|
||||
destructionLevelMap[destructionLevel] ?? unknownValue,
|
||||
causedBy,
|
||||
),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static LogAnalyzeLineData? _parseActorDeath(
|
||||
String line,
|
||||
String playerName,
|
||||
bool shouldCount,
|
||||
void Function(bool isKill, bool isDeath, bool isSelfKill) onDeath,
|
||||
) {
|
||||
final match = _actorDeathPattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final victimId = match.group(1) ?? unknownValue;
|
||||
final zone = match.group(2) ?? unknownValue;
|
||||
final killerId = match.group(3) ?? unknownValue;
|
||||
final damageType = match.group(4) ?? unknownValue;
|
||||
|
||||
if (shouldCount) {
|
||||
if (victimId.trim() == killerId.trim()) {
|
||||
onDeath(false, false, true); // 自杀
|
||||
} else {
|
||||
final isDeath = victimId.trim() == playerName;
|
||||
final isKill = killerId.trim() == playerName;
|
||||
onDeath(isKill, isDeath, false);
|
||||
}
|
||||
}
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "actor_death",
|
||||
title: S.current.log_analyzer_filter_character_death,
|
||||
data: S.current.log_analyzer_death_details(victimId, damageType, killerId, zone),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
victimId: victimId, // 格式化字段
|
||||
killerId: killerId, // 格式化字段
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static LogAnalyzeLineData? _parseCharacterName(String line) {
|
||||
final match = _characterNamePattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final characterName = match.group(1)?.trim() ?? unknownValue;
|
||||
return LogAnalyzeLineData(
|
||||
type: "player_login",
|
||||
title: S.current.log_analyzer_player_login(characterName),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
playerName: characterName, // 格式化字段
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static LogAnalyzeLineData? _parseRequestLocationInventory(String line) {
|
||||
final match = _requestLocationInventoryPattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final playerId = match.group(1) ?? unknownValue;
|
||||
final location = match.group(2) ?? unknownValue;
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "request_location_inventory",
|
||||
title: S.current.log_analyzer_view_local_inventory,
|
||||
data: S.current.log_analyzer_player_location(playerId, location),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
location: location, // 格式化字段
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,9 @@
|
||||
import '../frb_generated.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `get_process_path`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`
|
||||
|
||||
Future<void> sendNotify({
|
||||
String? summary,
|
||||
String? body,
|
||||
@ -22,3 +25,37 @@ Future<bool> setForegroundWindow({required String windowName}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateApiWin32ApiSetForegroundWindow(windowName: windowName);
|
||||
|
||||
Future<int> getProcessPidByName({required String processName}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateApiWin32ApiGetProcessPidByName(processName: processName);
|
||||
|
||||
Future<List<ProcessInfo>> getProcessListByName({required String processName}) =>
|
||||
RustLib.instance.api.crateApiWin32ApiGetProcessListByName(
|
||||
processName: processName,
|
||||
);
|
||||
|
||||
class ProcessInfo {
|
||||
final int pid;
|
||||
final String name;
|
||||
final String path;
|
||||
|
||||
const ProcessInfo({
|
||||
required this.pid,
|
||||
required this.name,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => pid.hashCode ^ name.hashCode ^ path.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ProcessInfo &&
|
||||
runtimeType == other.runtimeType &&
|
||||
pid == other.pid &&
|
||||
name == other.name &&
|
||||
path == other.path;
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
String get codegenVersion => '2.11.1';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -706588047;
|
||||
int get rustContentHash => 1227557070;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
@ -95,6 +95,14 @@ abstract class RustLibApi extends BaseApi {
|
||||
bool? withCustomDns,
|
||||
});
|
||||
|
||||
Future<List<ProcessInfo>> crateApiWin32ApiGetProcessListByName({
|
||||
required String processName,
|
||||
});
|
||||
|
||||
Future<int> crateApiWin32ApiGetProcessPidByName({
|
||||
required String processName,
|
||||
});
|
||||
|
||||
Future<RsiLauncherAsarData> crateApiAsarApiGetRsiLauncherAsarData({
|
||||
required String asarPath,
|
||||
});
|
||||
@ -280,6 +288,66 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<ProcessInfo>> crateApiWin32ApiGetProcessListByName({
|
||||
required String processName,
|
||||
}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
var arg0 = cst_encode_String(processName);
|
||||
return wire.wire__crate__api__win32_api__get_process_list_by_name(
|
||||
port_,
|
||||
arg0,
|
||||
);
|
||||
},
|
||||
codec: DcoCodec(
|
||||
decodeSuccessData: dco_decode_list_process_info,
|
||||
decodeErrorData: dco_decode_AnyhowException,
|
||||
),
|
||||
constMeta: kCrateApiWin32ApiGetProcessListByNameConstMeta,
|
||||
argValues: [processName],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiWin32ApiGetProcessListByNameConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "get_process_list_by_name",
|
||||
argNames: ["processName"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<int> crateApiWin32ApiGetProcessPidByName({
|
||||
required String processName,
|
||||
}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
var arg0 = cst_encode_String(processName);
|
||||
return wire.wire__crate__api__win32_api__get_process_pid_by_name(
|
||||
port_,
|
||||
arg0,
|
||||
);
|
||||
},
|
||||
codec: DcoCodec(
|
||||
decodeSuccessData: dco_decode_i_32,
|
||||
decodeErrorData: dco_decode_AnyhowException,
|
||||
),
|
||||
constMeta: kCrateApiWin32ApiGetProcessPidByNameConstMeta,
|
||||
argValues: [processName],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiWin32ApiGetProcessPidByNameConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "get_process_pid_by_name",
|
||||
argNames: ["processName"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<RsiLauncherAsarData> crateApiAsarApiGetRsiLauncherAsarData({
|
||||
required String asarPath,
|
||||
@ -722,6 +790,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return raw as Uint8List;
|
||||
}
|
||||
|
||||
@protected
|
||||
List<ProcessInfo> dco_decode_list_process_info(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return (raw as List<dynamic>).map(dco_decode_process_info).toList();
|
||||
}
|
||||
|
||||
@protected
|
||||
List<(String, String)> dco_decode_list_record_string_string(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@ -770,6 +844,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return raw == null ? null : dco_decode_list_prim_u_8_strict(raw);
|
||||
}
|
||||
|
||||
@protected
|
||||
ProcessInfo dco_decode_process_info(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 3)
|
||||
throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
||||
return ProcessInfo(
|
||||
pid: dco_decode_u_32(arr[0]),
|
||||
name: dco_decode_String(arr[1]),
|
||||
path: dco_decode_String(arr[2]),
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
(String, String) dco_decode_record_string_string(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@ -949,6 +1036,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
return deserializer.buffer.getUint8List(len_);
|
||||
}
|
||||
|
||||
@protected
|
||||
List<ProcessInfo> sse_decode_list_process_info(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
var len_ = sse_decode_i_32(deserializer);
|
||||
var ans_ = <ProcessInfo>[];
|
||||
for (var idx_ = 0; idx_ < len_; ++idx_) {
|
||||
ans_.add(sse_decode_process_info(deserializer));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
|
||||
@protected
|
||||
List<(String, String)> sse_decode_list_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
@ -1034,6 +1133,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
ProcessInfo sse_decode_process_info(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var var_pid = sse_decode_u_32(deserializer);
|
||||
var var_name = sse_decode_String(deserializer);
|
||||
var var_path = sse_decode_String(deserializer);
|
||||
return ProcessInfo(pid: var_pid, name: var_name, path: var_path);
|
||||
}
|
||||
|
||||
@protected
|
||||
(String, String) sse_decode_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
@ -1295,6 +1403,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
serializer.buffer.putUint8List(self);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_process_info(
|
||||
List<ProcessInfo> self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_32(self.length, serializer);
|
||||
for (final item in self) {
|
||||
sse_encode_process_info(item, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_list_record_string_string(
|
||||
List<(String, String)> self,
|
||||
@ -1378,6 +1498,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_process_info(ProcessInfo self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_u_32(self.pid, serializer);
|
||||
sse_encode_String(self.name, serializer);
|
||||
sse_encode_String(self.path, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_string(
|
||||
(String, String) self,
|
||||
|
||||
@ -62,6 +62,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
Uint8List dco_decode_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<ProcessInfo> dco_decode_list_process_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
List<(String, String)> dco_decode_list_record_string_string(dynamic raw);
|
||||
|
||||
@ -86,6 +89,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
|
||||
|
||||
@protected
|
||||
ProcessInfo dco_decode_process_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
(String, String) dco_decode_record_string_string(dynamic raw);
|
||||
|
||||
@ -159,6 +165,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<ProcessInfo> sse_decode_list_process_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
List<(String, String)> sse_decode_list_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
@ -187,6 +196,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ProcessInfo sse_decode_process_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
(String, String) sse_decode_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
@ -315,6 +327,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
return ans;
|
||||
}
|
||||
|
||||
@protected
|
||||
ffi.Pointer<wire_cst_list_process_info> cst_encode_list_process_info(
|
||||
List<ProcessInfo> raw,
|
||||
) {
|
||||
// Codec=Cst (C-struct based), see doc to use other codecs
|
||||
final ans = wire.cst_new_list_process_info(raw.length);
|
||||
for (var i = 0; i < raw.length; ++i) {
|
||||
cst_api_fill_to_wire_process_info(raw[i], ans.ref.ptr[i]);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
@protected
|
||||
ffi.Pointer<wire_cst_list_record_string_string>
|
||||
cst_encode_list_record_string_string(List<(String, String)> raw) {
|
||||
@ -374,6 +398,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
cst_api_fill_to_wire_rsi_launcher_asar_data(apiObj, wireObj.ref);
|
||||
}
|
||||
|
||||
@protected
|
||||
void cst_api_fill_to_wire_process_info(
|
||||
ProcessInfo apiObj,
|
||||
wire_cst_process_info wireObj,
|
||||
) {
|
||||
wireObj.pid = cst_encode_u_32(apiObj.pid);
|
||||
wireObj.name = cst_encode_String(apiObj.name);
|
||||
wireObj.path = cst_encode_String(apiObj.path);
|
||||
}
|
||||
|
||||
@protected
|
||||
void cst_api_fill_to_wire_record_string_string(
|
||||
(String, String) apiObj,
|
||||
@ -499,6 +533,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_process_info(
|
||||
List<ProcessInfo> self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_list_record_string_string(
|
||||
List<(String, String)> self,
|
||||
@ -532,6 +572,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_process_info(ProcessInfo self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_string(
|
||||
(String, String) self,
|
||||
@ -719,6 +762,60 @@ class RustLibWire implements BaseWire {
|
||||
)
|
||||
>();
|
||||
|
||||
void wire__crate__api__win32_api__get_process_list_by_name(
|
||||
int port_,
|
||||
ffi.Pointer<wire_cst_list_prim_u_8_strict> process_name,
|
||||
) {
|
||||
return _wire__crate__api__win32_api__get_process_list_by_name(
|
||||
port_,
|
||||
process_name,
|
||||
);
|
||||
}
|
||||
|
||||
late final _wire__crate__api__win32_api__get_process_list_by_namePtr =
|
||||
_lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Int64,
|
||||
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
|
||||
)
|
||||
>
|
||||
>(
|
||||
'frbgen_starcitizen_doctor_wire__crate__api__win32_api__get_process_list_by_name',
|
||||
);
|
||||
late final _wire__crate__api__win32_api__get_process_list_by_name =
|
||||
_wire__crate__api__win32_api__get_process_list_by_namePtr
|
||||
.asFunction<
|
||||
void Function(int, ffi.Pointer<wire_cst_list_prim_u_8_strict>)
|
||||
>();
|
||||
|
||||
void wire__crate__api__win32_api__get_process_pid_by_name(
|
||||
int port_,
|
||||
ffi.Pointer<wire_cst_list_prim_u_8_strict> process_name,
|
||||
) {
|
||||
return _wire__crate__api__win32_api__get_process_pid_by_name(
|
||||
port_,
|
||||
process_name,
|
||||
);
|
||||
}
|
||||
|
||||
late final _wire__crate__api__win32_api__get_process_pid_by_namePtr =
|
||||
_lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Void Function(
|
||||
ffi.Int64,
|
||||
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
|
||||
)
|
||||
>
|
||||
>(
|
||||
'frbgen_starcitizen_doctor_wire__crate__api__win32_api__get_process_pid_by_name',
|
||||
);
|
||||
late final _wire__crate__api__win32_api__get_process_pid_by_name =
|
||||
_wire__crate__api__win32_api__get_process_pid_by_namePtr
|
||||
.asFunction<
|
||||
void Function(int, ffi.Pointer<wire_cst_list_prim_u_8_strict>)
|
||||
>();
|
||||
|
||||
void wire__crate__api__asar_api__get_rsi_launcher_asar_data(
|
||||
int port_,
|
||||
ffi.Pointer<wire_cst_list_prim_u_8_strict> asar_path,
|
||||
@ -1144,6 +1241,19 @@ class RustLibWire implements BaseWire {
|
||||
late final _cst_new_list_prim_u_8_strict = _cst_new_list_prim_u_8_strictPtr
|
||||
.asFunction<ffi.Pointer<wire_cst_list_prim_u_8_strict> Function(int)>();
|
||||
|
||||
ffi.Pointer<wire_cst_list_process_info> cst_new_list_process_info(int len) {
|
||||
return _cst_new_list_process_info(len);
|
||||
}
|
||||
|
||||
late final _cst_new_list_process_infoPtr =
|
||||
_lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<wire_cst_list_process_info> Function(ffi.Int32)
|
||||
>
|
||||
>('frbgen_starcitizen_doctor_cst_new_list_process_info');
|
||||
late final _cst_new_list_process_info = _cst_new_list_process_infoPtr
|
||||
.asFunction<ffi.Pointer<wire_cst_list_process_info> Function(int)>();
|
||||
|
||||
ffi.Pointer<wire_cst_list_record_string_string>
|
||||
cst_new_list_record_string_string(int len) {
|
||||
return _cst_new_list_record_string_string(len);
|
||||
@ -1224,6 +1334,22 @@ final class wire_cst_list_String extends ffi.Struct {
|
||||
external int len;
|
||||
}
|
||||
|
||||
final class wire_cst_process_info extends ffi.Struct {
|
||||
@ffi.Uint32()
|
||||
external int pid;
|
||||
|
||||
external ffi.Pointer<wire_cst_list_prim_u_8_strict> name;
|
||||
|
||||
external ffi.Pointer<wire_cst_list_prim_u_8_strict> path;
|
||||
}
|
||||
|
||||
final class wire_cst_list_process_info extends ffi.Struct {
|
||||
external ffi.Pointer<wire_cst_process_info> ptr;
|
||||
|
||||
@ffi.Int32()
|
||||
external int len;
|
||||
}
|
||||
|
||||
final class wire_cst_rs_process_stream_data extends ffi.Struct {
|
||||
@ffi.Int32()
|
||||
external int data_type;
|
||||
|
||||
@ -7,7 +7,7 @@ import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_list_page.da
|
||||
import 'package:starcitizen_doctor/ui/party_room/widgets/detail/party_room_detail_page.dart';
|
||||
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_register_page.dart';
|
||||
|
||||
class PartyRoomUI extends HookConsumerWidget {
|
||||
class PartyRoomUI extends ConsumerWidget {
|
||||
const PartyRoomUI({super.key});
|
||||
|
||||
@override
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart';
|
||||
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||
import 'package:starcitizen_doctor/ui/party_room/utils/party_room_utils.dart' show PartyRoomUtils;
|
||||
|
||||
import 'utils/game_log_tracker_provider.dart';
|
||||
|
||||
part 'party_room_ui_model.freezed.dart';
|
||||
|
||||
@ -37,6 +42,7 @@ sealed class PartyRoomUIState with _$PartyRoomUIState {
|
||||
@riverpod
|
||||
class PartyRoomUIModel extends _$PartyRoomUIModel {
|
||||
Timer? _reconnectTimer;
|
||||
ProviderSubscription? _gameLogSubscription;
|
||||
|
||||
@override
|
||||
PartyRoomUIState build() {
|
||||
@ -48,18 +54,59 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
|
||||
if (previous?.room.isInRoom == true && !next.room.isInRoom) {
|
||||
state = state.copyWith(isMinimized: false);
|
||||
}
|
||||
|
||||
// 监听房间创建时间变化,设置游戏日志监听
|
||||
if (previous?.room.currentRoom?.createdAt != next.room.currentRoom?.createdAt) {
|
||||
_setupGameLogListener(next.room.currentRoom?.createdAt);
|
||||
}
|
||||
});
|
||||
|
||||
connectToServer();
|
||||
|
||||
// 在 dispose 时清理定时器
|
||||
// 在 dispose 时清理定时器和订阅
|
||||
ref.onDispose(() {
|
||||
_reconnectTimer?.cancel();
|
||||
_gameLogSubscription?.close();
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// 设置游戏日志监听
|
||||
void _setupGameLogListener(Int64? createdAt) {
|
||||
// 清除之前的订阅
|
||||
_gameLogSubscription?.close();
|
||||
_gameLogSubscription = null;
|
||||
|
||||
final dateTime = PartyRoomUtils.getDateTime(createdAt);
|
||||
if (dateTime != null) {
|
||||
_gameLogSubscription = ref.listen<PartyRoomGameLogTrackerProviderState>(
|
||||
partyRoomGameLogTrackerProviderProvider(startTime: dateTime),
|
||||
(previous, next) => _onUpdateGameStatus(previous, next),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理游戏状态更新
|
||||
void _onUpdateGameStatus(PartyRoomGameLogTrackerProviderState? previous, PartyRoomGameLogTrackerProviderState next) {
|
||||
// 防抖
|
||||
final currentGameStartTime = previous?.gameStartTime?.millisecondsSinceEpoch;
|
||||
final gameStartTime = next.gameStartTime?.microsecondsSinceEpoch;
|
||||
if (next.kills != previous?.kills ||
|
||||
next.deaths != previous?.deaths ||
|
||||
next.location != previous?.location ||
|
||||
currentGameStartTime != gameStartTime) {
|
||||
// 更新状态
|
||||
ref
|
||||
.read(partyRoomProvider.notifier)
|
||||
.setStatus(
|
||||
kills: next.kills != previous?.kills ? next.kills : null,
|
||||
deaths: next.deaths != previous?.deaths ? next.deaths : null,
|
||||
currentLocation: next.location != previous?.location ? next.location : null,
|
||||
playTime: currentGameStartTime != gameStartTime ? gameStartTime : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理连接状态变化
|
||||
void _handleConnectionStateChange(PartyRoomFullState? previous, PartyRoomFullState next) {
|
||||
// 检测断线:之前已连接但现在未连接
|
||||
@ -140,17 +187,16 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
|
||||
|
||||
// 加载标签(游客和登录用户都需要)
|
||||
await partyRoom.loadTags();
|
||||
|
||||
// 非游客模式:尝试登录
|
||||
try {
|
||||
state = state.copyWith(isLoggingIn: true);
|
||||
await partyRoom.login();
|
||||
// 登录成功,加载房间列表
|
||||
await loadRoomList();
|
||||
state = state.copyWith(showRoomList: true);
|
||||
state = state.copyWith(showRoomList: true, isLoggingIn: false, isGuestMode: false);
|
||||
} catch (e) {
|
||||
// 未注册,保持在连接状态
|
||||
dPrint('[PartyRoomUI] Login failed, need register: $e');
|
||||
state = state.copyWith(isGuestMode: true);
|
||||
} finally {
|
||||
state = state.copyWith(isLoggingIn: false);
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ final class PartyRoomUIModelProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$partyRoomUIModelHash() => r'48291373cafc9005843478a90970152426b3a666';
|
||||
String _$partyRoomUIModelHash() => r'a0b6c3632ff33f2d58882f9bc1ab58c69c2487f4';
|
||||
|
||||
abstract class _$PartyRoomUIModel extends $Notifier<PartyRoomUIState> {
|
||||
PartyRoomUIState build();
|
||||
|
||||
172
lib/ui/party_room/utils/game_log_tracker_provider.dart
Normal file
172
lib/ui/party_room/utils/game_log_tracker_provider.dart
Normal file
@ -0,0 +1,172 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
|
||||
import 'package:starcitizen_doctor/common/rust/api/win32_api.dart' as win32;
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
|
||||
part 'game_log_tracker_provider.freezed.dart';
|
||||
|
||||
part 'game_log_tracker_provider.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class PartyRoomGameLogTrackerProviderState with _$PartyRoomGameLogTrackerProviderState {
|
||||
const factory PartyRoomGameLogTrackerProviderState({
|
||||
@Default('') String location,
|
||||
@Default(0) int kills,
|
||||
@Default(0) int deaths,
|
||||
DateTime? gameStartTime,
|
||||
@Default([]) List<String> killedIds, // 本次迭代新增的击杀ID
|
||||
@Default([]) List<String> deathIds, // 本次迭代新增的死亡ID
|
||||
}) = _PartyRoomGameLogTrackerProviderState;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider {
|
||||
var _disposed = false;
|
||||
|
||||
// 记录上次查询的时间点,用于计算增量
|
||||
DateTime? _lastQueryTime;
|
||||
|
||||
@override
|
||||
PartyRoomGameLogTrackerProviderState build({required DateTime startTime}) {
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] init $startTime");
|
||||
ref.onDispose(() {
|
||||
_disposed = true;
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] disposed $startTime");
|
||||
});
|
||||
_lastQueryTime = startTime;
|
||||
_listenToGameLogs(startTime);
|
||||
return const PartyRoomGameLogTrackerProviderState();
|
||||
}
|
||||
|
||||
Future<void> _listenToGameLogs(DateTime startTime) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
while (!_disposed) {
|
||||
try {
|
||||
// 获取正在运行的游戏进程
|
||||
final l = await win32.getProcessListByName(processName: "StarCitizen.exe");
|
||||
final p = l
|
||||
.where((e) => e.path.toLowerCase().contains("starcitizen") && e.path.toLowerCase().contains("bin64"))
|
||||
.firstOrNull;
|
||||
|
||||
if (p == null) throw Exception("process not found");
|
||||
|
||||
final logPath = _getLogPath(p);
|
||||
final logFile = File(logPath);
|
||||
|
||||
if (await logFile.exists()) {
|
||||
// 分析日志文件
|
||||
await _analyzeLog(logFile, startTime);
|
||||
} else {
|
||||
state = state.copyWith(location: '<Unknown>', gameStartTime: null);
|
||||
}
|
||||
} catch (e) {
|
||||
// 游戏未启动或发生错误
|
||||
state = state.copyWith(
|
||||
location: '<游戏未启动>',
|
||||
gameStartTime: null,
|
||||
kills: 0,
|
||||
deaths: 0,
|
||||
killedIds: [],
|
||||
deathIds: [],
|
||||
);
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _analyzeLog(File logFile, DateTime startTime) async {
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
|
||||
// 使用 GameLogAnalyzer 分析日志
|
||||
// startTime 只影响计数统计
|
||||
final result = await GameLogAnalyzer.analyzeLogFile(logFile, startTime: startTime);
|
||||
final (logData, statistics) = result;
|
||||
|
||||
// 从统计数据中直接获取最新位置(全量查找的结果)
|
||||
final location = statistics.latestLocation == null ? '<主菜单>' : '[${statistics.latestLocation}]';
|
||||
|
||||
// 计算基于 _lastQueryTime 之后的增量 ID
|
||||
final newKilledIds = <String>[];
|
||||
final newDeathIds = <String>[];
|
||||
|
||||
if (_lastQueryTime != null) {
|
||||
// 遍历所有 actor_death 事件
|
||||
for (final data in logData) {
|
||||
if (data.type != "actor_death") continue;
|
||||
|
||||
// 解析事件时间
|
||||
if (data.dateTime != null) {
|
||||
try {
|
||||
// 日志时间格式: "yyyy-MM-dd HH:mm:ss:SSS"
|
||||
// 转换为 ISO 8601 格式再解析
|
||||
final parts = data.dateTime!.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
final datePart = parts[0]; // yyyy-MM-dd
|
||||
final timeParts = parts[1].split(':');
|
||||
if (timeParts.length >= 3) {
|
||||
final hour = timeParts[0];
|
||||
final minute = timeParts[1];
|
||||
final secondMillis = timeParts[2]; // ss:SSS 或 ss.SSS
|
||||
final timeStr = '$datePart $hour:$minute:${secondMillis.replaceAll(':', '.')}';
|
||||
final eventTime = DateTime.parse(timeStr);
|
||||
|
||||
// 只处理在 _lastQueryTime 之后的事件
|
||||
if (eventTime.isBefore(_lastQueryTime!)) continue;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] Failed to parse dateTime: ${data.dateTime}, error: $e");
|
||||
// 时间解析失败,继续处理该事件(保守策略)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用格式化字段,不再重新解析
|
||||
final victimId = data.victimId;
|
||||
final killerId = data.killerId;
|
||||
|
||||
if (victimId != null && killerId != null && victimId != killerId) {
|
||||
// 如果玩家是击杀者,记录被击杀的ID
|
||||
if (killerId == statistics.playerName) {
|
||||
newKilledIds.add(victimId);
|
||||
}
|
||||
|
||||
// 如果玩家是受害者,记录击杀者ID
|
||||
if (victimId == statistics.playerName) {
|
||||
newDeathIds.add(killerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新状态,只存储本次迭代的增量数据
|
||||
state = state.copyWith(
|
||||
location: location,
|
||||
kills: statistics.killCount,
|
||||
// 从 startTime 开始的总计数
|
||||
deaths: statistics.deathCount,
|
||||
// 从 startTime 开始的总计数
|
||||
gameStartTime: statistics.gameStartTime,
|
||||
// 全量查找的游戏开始时间
|
||||
killedIds: newKilledIds,
|
||||
// 只存储本次迭代的增量
|
||||
deathIds: newDeathIds, // 只存储本次迭代的增量
|
||||
);
|
||||
|
||||
// 更新查询时间为本次查询的时刻
|
||||
_lastQueryTime = now;
|
||||
} catch (e, stackTrace) {
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] Error analyzing log: $e");
|
||||
dPrint("[PartyRoomGameLogTrackerProvider] Stack trace: $stackTrace");
|
||||
}
|
||||
}
|
||||
|
||||
String _getLogPath(win32.ProcessInfo p) {
|
||||
var path = p.path;
|
||||
return path.replaceAll(r"Bin64\StarCitizen.exe", "Game.log");
|
||||
}
|
||||
}
|
||||
295
lib/ui/party_room/utils/game_log_tracker_provider.freezed.dart
Normal file
295
lib/ui/party_room/utils/game_log_tracker_provider.freezed.dart
Normal file
@ -0,0 +1,295 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'game_log_tracker_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$PartyRoomGameLogTrackerProviderState {
|
||||
|
||||
String get location; int get kills; int get deaths; DateTime? get gameStartTime; List<String> get killedIds;// 本次迭代新增的击杀ID
|
||||
List<String> get deathIds;
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PartyRoomGameLogTrackerProviderStateCopyWith<PartyRoomGameLogTrackerProviderState> get copyWith => _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<PartyRoomGameLogTrackerProviderState>(this as PartyRoomGameLogTrackerProviderState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PartyRoomGameLogTrackerProviderState&&(identical(other.location, location) || other.location == location)&&(identical(other.kills, kills) || other.kills == kills)&&(identical(other.deaths, deaths) || other.deaths == deaths)&&(identical(other.gameStartTime, gameStartTime) || other.gameStartTime == gameStartTime)&&const DeepCollectionEquality().equals(other.killedIds, killedIds)&&const DeepCollectionEquality().equals(other.deathIds, deathIds));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(killedIds),const DeepCollectionEquality().hash(deathIds));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
factory $PartyRoomGameLogTrackerProviderStateCopyWith(PartyRoomGameLogTrackerProviderState value, $Res Function(PartyRoomGameLogTrackerProviderState) _then) = _$PartyRoomGameLogTrackerProviderStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
|
||||
implements $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
_$PartyRoomGameLogTrackerProviderStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PartyRoomGameLogTrackerProviderState _self;
|
||||
final $Res Function(PartyRoomGameLogTrackerProviderState) _then;
|
||||
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? killedIds = null,Object? deathIds = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as String,kills: null == kills ? _self.kills : kills // ignore: cast_nullable_to_non_nullable
|
||||
as int,deaths: null == deaths ? _self.deaths : deaths // ignore: cast_nullable_to_non_nullable
|
||||
as int,gameStartTime: freezed == gameStartTime ? _self.gameStartTime : gameStartTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,killedIds: null == killedIds ? _self.killedIds : killedIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,deathIds: null == deathIds ? _self.deathIds : deathIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PartyRoomGameLogTrackerProviderState].
|
||||
extension PartyRoomGameLogTrackerProviderStatePatterns on PartyRoomGameLogTrackerProviderState {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PartyRoomGameLogTrackerProviderState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PartyRoomGameLogTrackerProviderState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PartyRoomGameLogTrackerProviderState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState():
|
||||
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PartyRoomGameLogTrackerProviderState() when $default != null:
|
||||
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _PartyRoomGameLogTrackerProviderState implements PartyRoomGameLogTrackerProviderState {
|
||||
const _PartyRoomGameLogTrackerProviderState({this.location = '', this.kills = 0, this.deaths = 0, this.gameStartTime, final List<String> killedIds = const [], final List<String> deathIds = const []}): _killedIds = killedIds,_deathIds = deathIds;
|
||||
|
||||
|
||||
@override@JsonKey() final String location;
|
||||
@override@JsonKey() final int kills;
|
||||
@override@JsonKey() final int deaths;
|
||||
@override final DateTime? gameStartTime;
|
||||
final List<String> _killedIds;
|
||||
@override@JsonKey() List<String> get killedIds {
|
||||
if (_killedIds is EqualUnmodifiableListView) return _killedIds;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_killedIds);
|
||||
}
|
||||
|
||||
// 本次迭代新增的击杀ID
|
||||
final List<String> _deathIds;
|
||||
// 本次迭代新增的击杀ID
|
||||
@override@JsonKey() List<String> get deathIds {
|
||||
if (_deathIds is EqualUnmodifiableListView) return _deathIds;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_deathIds);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PartyRoomGameLogTrackerProviderStateCopyWith<_PartyRoomGameLogTrackerProviderState> get copyWith => __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<_PartyRoomGameLogTrackerProviderState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PartyRoomGameLogTrackerProviderState&&(identical(other.location, location) || other.location == location)&&(identical(other.kills, kills) || other.kills == kills)&&(identical(other.deaths, deaths) || other.deaths == deaths)&&(identical(other.gameStartTime, gameStartTime) || other.gameStartTime == gameStartTime)&&const DeepCollectionEquality().equals(other._killedIds, _killedIds)&&const DeepCollectionEquality().equals(other._deathIds, _deathIds));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(_killedIds),const DeepCollectionEquality().hash(_deathIds));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PartyRoomGameLogTrackerProviderStateCopyWith<$Res> implements $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
factory _$PartyRoomGameLogTrackerProviderStateCopyWith(_PartyRoomGameLogTrackerProviderState value, $Res Function(_PartyRoomGameLogTrackerProviderState) _then) = __$PartyRoomGameLogTrackerProviderStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
|
||||
implements _$PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
|
||||
__$PartyRoomGameLogTrackerProviderStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PartyRoomGameLogTrackerProviderState _self;
|
||||
final $Res Function(_PartyRoomGameLogTrackerProviderState) _then;
|
||||
|
||||
/// Create a copy of PartyRoomGameLogTrackerProviderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? killedIds = null,Object? deathIds = null,}) {
|
||||
return _then(_PartyRoomGameLogTrackerProviderState(
|
||||
location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as String,kills: null == kills ? _self.kills : kills // ignore: cast_nullable_to_non_nullable
|
||||
as int,deaths: null == deaths ? _self.deaths : deaths // ignore: cast_nullable_to_non_nullable
|
||||
as int,gameStartTime: freezed == gameStartTime ? _self.gameStartTime : gameStartTime // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,killedIds: null == killedIds ? _self._killedIds : killedIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,deathIds: null == deathIds ? _self._deathIds : deathIds // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
128
lib/ui/party_room/utils/game_log_tracker_provider.g.dart
Normal file
128
lib/ui/party_room/utils/game_log_tracker_provider.g.dart
Normal file
@ -0,0 +1,128 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'game_log_tracker_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PartyRoomGameLogTrackerProvider)
|
||||
const partyRoomGameLogTrackerProviderProvider =
|
||||
PartyRoomGameLogTrackerProviderFamily._();
|
||||
|
||||
final class PartyRoomGameLogTrackerProviderProvider
|
||||
extends
|
||||
$NotifierProvider<
|
||||
PartyRoomGameLogTrackerProvider,
|
||||
PartyRoomGameLogTrackerProviderState
|
||||
> {
|
||||
const PartyRoomGameLogTrackerProviderProvider._({
|
||||
required PartyRoomGameLogTrackerProviderFamily super.from,
|
||||
required DateTime super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'partyRoomGameLogTrackerProviderProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$partyRoomGameLogTrackerProviderHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'partyRoomGameLogTrackerProviderProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PartyRoomGameLogTrackerProvider create() => PartyRoomGameLogTrackerProvider();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PartyRoomGameLogTrackerProviderState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride:
|
||||
$SyncValueProvider<PartyRoomGameLogTrackerProviderState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PartyRoomGameLogTrackerProviderProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$partyRoomGameLogTrackerProviderHash() =>
|
||||
r'ecb015eb46d25bfe11bbb153242fd5c4f20ef367';
|
||||
|
||||
final class PartyRoomGameLogTrackerProviderFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
PartyRoomGameLogTrackerProvider,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
DateTime
|
||||
> {
|
||||
const PartyRoomGameLogTrackerProviderFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'partyRoomGameLogTrackerProviderProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
PartyRoomGameLogTrackerProviderProvider call({required DateTime startTime}) =>
|
||||
PartyRoomGameLogTrackerProviderProvider._(
|
||||
argument: startTime,
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'partyRoomGameLogTrackerProviderProvider';
|
||||
}
|
||||
|
||||
abstract class _$PartyRoomGameLogTrackerProvider
|
||||
extends $Notifier<PartyRoomGameLogTrackerProviderState> {
|
||||
late final _$args = ref.$arg as DateTime;
|
||||
DateTime get startTime => _$args;
|
||||
|
||||
PartyRoomGameLogTrackerProviderState build({required DateTime startTime});
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(startTime: _$args);
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
PartyRoomGameLogTrackerProviderState
|
||||
>,
|
||||
PartyRoomGameLogTrackerProviderState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ class PartyRoomHeader extends ConsumerWidget {
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.back, size: 16, color: Color(0xFFB5BAC1)),
|
||||
icon: const Icon(FluentIcons.back, size: 16, color: Colors.white),
|
||||
onPressed: () {
|
||||
ref.read(partyRoomUIModelProvider.notifier).setMinimized(true);
|
||||
},
|
||||
|
||||
@ -54,6 +54,7 @@ class PartyRoomMemberItem extends ConsumerWidget {
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
child: GestureDetector(
|
||||
onTapUp: (details) => _showMemberContextMenu(context, member, partyRoom, isOwner, isSelf, flyoutController),
|
||||
onSecondaryTapUp: (details) =>
|
||||
_showMemberContextMenu(context, member, partyRoom, isOwner, isSelf, flyoutController),
|
||||
child: HoverButton(
|
||||
@ -94,24 +95,23 @@ class PartyRoomMemberItem extends ConsumerWidget {
|
||||
],
|
||||
],
|
||||
),
|
||||
if (member.status.currentLocation.isNotEmpty)
|
||||
Text(
|
||||
member.status.currentLocation,
|
||||
style: const TextStyle(fontSize: 10, color: Color(0xFF80848E)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
member.status.currentLocation.isNotEmpty ? member.status.currentLocation : '...',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .9)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
"K: ${member.status.kills} D: ${member.status.deaths}",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 状态指示器
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF23A559), // 在线绿色
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -134,7 +134,7 @@ class PartyRoomMemberItem extends ConsumerWidget {
|
||||
// 复制ID - 所有用户可用
|
||||
MenuFlyoutItem(
|
||||
leading: const Icon(FluentIcons.copy, size: 16),
|
||||
text: const Text('复制用户ID'),
|
||||
text: const Text('复制游戏ID'),
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: member.gameUserId));
|
||||
},
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart' as
|
||||
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||
import 'package:starcitizen_doctor/widgets/src/cache_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// 消息列表组件
|
||||
class PartyRoomMessageList extends ConsumerWidget {
|
||||
@ -20,19 +21,17 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
final room = partyRoomState.room.currentRoom;
|
||||
final hasSocialLinks = room != null && room.socialLinks.isNotEmpty;
|
||||
|
||||
// 计算总项数:社交链接消息(如果有)+ 事件消息
|
||||
final totalItems = (hasSocialLinks ? 1 : 0) + events.length;
|
||||
// 计算总项数:社交链接消息(如果有)+ 复制 ID 消息 + 事件消息
|
||||
final totalItems = (hasSocialLinks ? 1 : 0) + 1 + events.length;
|
||||
|
||||
if (totalItems == 0) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(FluentIcons.chat, size: 64, color: Color(0xFF404249)),
|
||||
Icon(FluentIcons.chat, size: 64, color: Colors.white.withValues(alpha: .6)),
|
||||
const SizedBox(height: 16),
|
||||
Text('暂无消息', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14)),
|
||||
const SizedBox(height: 4),
|
||||
Text('发送一条信号开始对话吧!', style: TextStyle(color: Colors.white.withValues(alpha: 0.3), fontSize: 12)),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -47,9 +46,13 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
if (hasSocialLinks && index == 0) {
|
||||
return _buildSocialLinksMessage(room);
|
||||
}
|
||||
|
||||
// 第二条消息显示复制 ID
|
||||
final copyIdIndex = hasSocialLinks ? 1 : 0;
|
||||
if (index == copyIdIndex) {
|
||||
return _buildCopyIdMessage(room);
|
||||
}
|
||||
// 其他消息显示事件
|
||||
final eventIndex = hasSocialLinks ? index - 1 : index;
|
||||
final eventIndex = index - (hasSocialLinks ? 2 : 1);
|
||||
final event = events[eventIndex];
|
||||
return _MessageItem(event: event);
|
||||
},
|
||||
@ -78,7 +81,7 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'该房间包含第三方社交连接,点击加入一起开黑吧~',
|
||||
'该房间包含第三方社交连接,点击加入自由交流吧~',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFFDBDEE1), fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
@ -117,6 +120,68 @@ class PartyRoomMessageList extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCopyIdMessage(dynamic room) {
|
||||
final ownerGameId = room?.ownerGameId ?? '';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: const Color(0xFF2B2D31), borderRadius: BorderRadius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(color: Colors.purple, shape: BoxShape.circle),
|
||||
child: const Icon(FluentIcons.copy, size: 14, color: Colors.white),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'复制房主的游戏ID,可在游戏首页添加好友,快速组队',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFFDBDEE1), fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(color: const Color(0xFF1E1F22), borderRadius: BorderRadius.circular(4)),
|
||||
child: Text(ownerGameId, style: const TextStyle(fontSize: 13, color: Color(0xFFDBDEE1))),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Button(
|
||||
onPressed: ownerGameId.isNotEmpty
|
||||
? () async {
|
||||
await Clipboard.setData(ClipboardData(text: ownerGameId));
|
||||
}
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)),
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.isPressed) return const Color(0xFF3A40A0);
|
||||
if (states.isHovered) return const Color(0xFF4752C4);
|
||||
return const Color(0xFF5865F2);
|
||||
}),
|
||||
),
|
||||
child: const Text(
|
||||
'复制',
|
||||
style: TextStyle(fontSize: 13, color: Colors.white, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getSocialIcon(String link) {
|
||||
if (link.contains('qq.com')) return FontAwesomeIcons.qq;
|
||||
if (link.contains('discord')) return FontAwesomeIcons.discord;
|
||||
@ -145,6 +210,9 @@ class _MessageItem extends ConsumerWidget {
|
||||
final userName = _getEventUserName(roomEvent);
|
||||
final avatarUrl = _getEventAvatarUrl(roomEvent);
|
||||
|
||||
final text = _getEventText(roomEvent, ref);
|
||||
if (text == null) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
@ -176,7 +244,7 @@ class _MessageItem extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getEventText(roomEvent, ref),
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isSignal ? const Color(0xFFDBDEE1) : const Color(0xFF949BA4),
|
||||
@ -223,7 +291,7 @@ class _MessageItem extends ConsumerWidget {
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getEventText(partroom.RoomEvent event, WidgetRef ref) {
|
||||
String? _getEventText(partroom.RoomEvent event, WidgetRef ref) {
|
||||
final partyRoomState = ref.read(partyRoomProvider);
|
||||
final signalTypes = partyRoomState.room.signalTypes;
|
||||
switch (event.type) {
|
||||
@ -245,21 +313,13 @@ class _MessageItem extends ConsumerWidget {
|
||||
case partroom.RoomEventType.ROOM_UPDATED:
|
||||
return '房间信息已更新';
|
||||
case partroom.RoomEventType.MEMBER_STATUS_UPDATED:
|
||||
if (event.hasMember()) {
|
||||
final member = event.member;
|
||||
final name = member.handleName.isNotEmpty ? member.handleName : member.gameUserId;
|
||||
if (member.hasStatus() && member.status.currentLocation.isNotEmpty) {
|
||||
return '$name 更新了状态: ${member.status.currentLocation}';
|
||||
}
|
||||
return '$name 更新了状态';
|
||||
}
|
||||
return '成员状态已更新';
|
||||
return null;
|
||||
case partroom.RoomEventType.ROOM_DISMISSED:
|
||||
return '房间已解散';
|
||||
case partroom.RoomEventType.MEMBER_KICKED:
|
||||
return '被踢出房间';
|
||||
default:
|
||||
return '未知事件';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' show debugPrint;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
|
||||
import 'package:starcitizen_doctor/generated/l10n.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
part 'log_analyze_provider.g.dart';
|
||||
|
||||
part 'log_analyze_provider.freezed.dart';
|
||||
|
||||
final Map<String?, String> logAnalyzeSearchTypeMap = {
|
||||
null: S.current.log_analyzer_filter_all,
|
||||
"info": S.current.log_analyzer_filter_basic_info,
|
||||
@ -26,132 +21,29 @@ final Map<String?, String> logAnalyzeSearchTypeMap = {
|
||||
"request_location_inventory": S.current.log_analyzer_filter_local_inventory,
|
||||
};
|
||||
|
||||
@freezed
|
||||
abstract class LogAnalyzeLineData with _$LogAnalyzeLineData {
|
||||
const factory LogAnalyzeLineData({
|
||||
required String type,
|
||||
required String title,
|
||||
String? data,
|
||||
String? dateTime,
|
||||
}) = _LogAnalyzeLineData;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
||||
static const String unknownValue = "<Unknown>";
|
||||
|
||||
@override
|
||||
Future<List<LogAnalyzeLineData>> build(String gameInstallPath, bool listSortReverse) async {
|
||||
final logFile = File("$gameInstallPath/Game.log");
|
||||
debugPrint("[ToolsLogAnalyze] logFile: ${logFile.absolute.path}");
|
||||
if (gameInstallPath.isEmpty || !(await logFile.exists())) {
|
||||
return [
|
||||
LogAnalyzeLineData(
|
||||
type: "error",
|
||||
title: S.current.log_analyzer_no_log_file,
|
||||
)
|
||||
];
|
||||
return [const LogAnalyzeLineData(type: "error", title: "未找到日志文件")];
|
||||
}
|
||||
state = AsyncData([]);
|
||||
state = const AsyncData([]);
|
||||
_launchLogAnalyze(logFile);
|
||||
return state.value ?? [];
|
||||
}
|
||||
|
||||
String _playerName = ""; // 记录玩家名称
|
||||
int _killCount = 0; // 记录击杀其他实体次数
|
||||
int _deathCount = 0; // 记录被击杀次数
|
||||
int _selfKillCount = 0; // 记录自杀次数
|
||||
int _vehicleDestructionCount = 0; // 记录载具损毁次数 (软死亡)
|
||||
int _vehicleDestructionCountHard = 0; // 记录载具损毁次数 (解体)
|
||||
DateTime? _gameStartTime; // 记录游戏开始时间
|
||||
int _gameCrashLineNumber = -1; // 记录${S.current.log_analyzer_filter_game_crash}行号
|
||||
int _currentLineNumber = 0; // 当前行号
|
||||
void _launchLogAnalyze(File logFile) async {
|
||||
// 使用新的 GameLogAnalyzer 工具类
|
||||
final result = await GameLogAnalyzer.analyzeLogFile(logFile);
|
||||
final (results, _) = result;
|
||||
|
||||
void _launchLogAnalyze(File logFile, {int startLine = 0}) async {
|
||||
final logLines = utf8.decode((await logFile.readAsBytes()), allowMalformed: true).split("\n");
|
||||
debugPrint("[ToolsLogAnalyze] logLines: ${logLines.length}");
|
||||
if (startLine == 0) {
|
||||
_killCount = 0;
|
||||
_deathCount = 0;
|
||||
_selfKillCount = 0;
|
||||
_vehicleDestructionCount = 0;
|
||||
_vehicleDestructionCountHard = 0;
|
||||
_gameStartTime = null;
|
||||
_gameCrashLineNumber = -1;
|
||||
} else if (startLine > logLines.length) {
|
||||
// 考虑文件重新写入的情况
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
_currentLineNumber = logLines.length;
|
||||
// for i in logLines
|
||||
for (var i = 0; i < logLines.length; i++) {
|
||||
// 支持追加模式
|
||||
if (i < startLine) continue;
|
||||
final line = logLines[i];
|
||||
if (line.isEmpty) continue;
|
||||
final data = _handleLogLine(line, i);
|
||||
if (data != null) {
|
||||
_appendResult(data);
|
||||
// wait for ui update
|
||||
await Future.delayed(Duration(seconds: 0));
|
||||
}
|
||||
}
|
||||
|
||||
final lastLineDateTime =
|
||||
_gameStartTime != null ? _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20"))) : null;
|
||||
|
||||
// 检查${S.current.log_analyzer_filter_game_crash}行号
|
||||
if (_gameCrashLineNumber > 0) {
|
||||
// crashInfo 从 logLines _gameCrashLineNumber 开始到最后一行
|
||||
final crashInfo = logLines.sublist(_gameCrashLineNumber);
|
||||
// 运行一键诊断
|
||||
final info = SCLoggerHelper.getGameRunningLogInfo(crashInfo);
|
||||
crashInfo.add(S.current.log_analyzer_one_click_diagnosis_header);
|
||||
if (info != null) {
|
||||
crashInfo.add(info.key);
|
||||
if (info.value.isNotEmpty) {
|
||||
crashInfo.add(S.current.log_analyzer_details_info(info.value));
|
||||
}
|
||||
} else {
|
||||
crashInfo.add(S.current.log_analyzer_no_crash_detected);
|
||||
}
|
||||
_appendResult(LogAnalyzeLineData(
|
||||
type: "game_crash",
|
||||
title: S.current.log_analyzer_game_crash,
|
||||
data: crashInfo.join("\n"),
|
||||
dateTime: lastLineDateTime != null ? _dateTimeFormatter.format(lastLineDateTime) : null,
|
||||
));
|
||||
}
|
||||
|
||||
// ${S.current.log_analyzer_kill_summary}
|
||||
if (_killCount > 0 || _deathCount > 0) {
|
||||
_appendResult(LogAnalyzeLineData(
|
||||
type: "statistics",
|
||||
title: S.current.log_analyzer_kill_summary,
|
||||
data: S.current.log_analyzer_kill_death_suicide_count(
|
||||
_killCount,
|
||||
_deathCount,
|
||||
_selfKillCount,
|
||||
_vehicleDestructionCount,
|
||||
_vehicleDestructionCountHard,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// 统计${S.current.log_analyzer_play_time},_gameStartTime 减去 最后一行的时间
|
||||
if (_gameStartTime != null) {
|
||||
if (lastLineDateTime != null) {
|
||||
final duration = lastLineDateTime.difference(_gameStartTime!);
|
||||
_appendResult(LogAnalyzeLineData(
|
||||
type: "statistics",
|
||||
title: S.current.log_analyzer_play_time,
|
||||
data: S.current.log_analyzer_play_time_format(
|
||||
duration.inHours,
|
||||
duration.inMinutes.remainder(60),
|
||||
duration.inSeconds.remainder(60),
|
||||
),
|
||||
));
|
||||
}
|
||||
// 逐条添加结果以支持流式显示
|
||||
for (final data in results) {
|
||||
_appendResult(data);
|
||||
await Future.delayed(Duration.zero); // 让 UI 有机会更新
|
||||
}
|
||||
|
||||
_startListenFile(logFile);
|
||||
@ -165,21 +57,21 @@ class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
||||
debugPrint("[ToolsLogAnalyze] startListenFile: ${logFile.absolute.path}");
|
||||
// 监听文件
|
||||
late final StreamSubscription sub;
|
||||
sub = FileWatcher(logFile.absolute.path, pollingDelay: Duration(seconds: 1)).events.listen((change) {
|
||||
sub = FileWatcher(logFile.absolute.path, pollingDelay: const Duration(seconds: 1)).events.listen((change) {
|
||||
sub.cancel();
|
||||
if (!_isListenEnabled) return;
|
||||
_isListenEnabled = false;
|
||||
debugPrint("[ToolsLogAnalyze] logFile change: ${change.type}");
|
||||
switch (change.type) {
|
||||
case ChangeType.MODIFY:
|
||||
// 移除${S.current.log_analyzer_filter_statistics}
|
||||
// 移除统计信息
|
||||
final newList = state.value?.where((e) => e.type != "statistics").toList();
|
||||
if (listSortReverse) {
|
||||
state = AsyncData(newList?.reversed.toList() ?? []);
|
||||
} else {
|
||||
state = AsyncData(newList ?? []);
|
||||
}
|
||||
return _launchLogAnalyze(logFile, startLine: _currentLineNumber);
|
||||
return _launchLogAnalyze(logFile);
|
||||
case ChangeType.ADD:
|
||||
case ChangeType.REMOVE:
|
||||
ref.invalidateSelf();
|
||||
@ -191,67 +83,6 @@ class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
||||
});
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _handleLogLine(String line, int index) {
|
||||
// 处理 log 行,检测可以提取的内容
|
||||
if (_gameStartTime == null) {
|
||||
_gameStartTime = _getLogLineDateTime(line);
|
||||
return LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: S.current.log_analyzer_game_start,
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
// 读取${S.current.log_analyzer_game_loading}时间
|
||||
final gameLoading = _logGetGameLoading(line);
|
||||
if (gameLoading != null) {
|
||||
return LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: S.current.log_analyzer_game_loading,
|
||||
data: S.current.log_analyzer_mode_loading_time(
|
||||
gameLoading.$1,
|
||||
gameLoading.$2,
|
||||
),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
// 运行基础时间解析器
|
||||
final baseEvent = _baseEventDecoder(line);
|
||||
if (baseEvent != null) {
|
||||
switch (baseEvent) {
|
||||
case "AccountLoginCharacterStatus_Character":
|
||||
// 角色登录
|
||||
return _logGetCharacterName(line);
|
||||
case "FatalCollision":
|
||||
// 载具${S.current.log_analyzer_filter_fatal_collision}
|
||||
return _logGetFatalCollision(line);
|
||||
case "Vehicle Destruction":
|
||||
// ${S.current.log_analyzer_filter_vehicle_damaged}
|
||||
return _logGetVehicleDestruction(line);
|
||||
case "Actor Death":
|
||||
// ${S.current.log_analyzer_filter_character_death}
|
||||
return _logGetActorDeath(line);
|
||||
case "RequestLocationInventory":
|
||||
// 请求${S.current.log_analyzer_filter_local_inventory}
|
||||
return _logGetRequestLocationInventory(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (line.contains("[CIG] CCIGBroker::FastShutdown")) {
|
||||
return LogAnalyzeLineData(
|
||||
type: "info",
|
||||
title: S.current.log_analyzer_game_close,
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
if (line.contains("Cloud Imperium Games public crash handler")) {
|
||||
_gameCrashLineNumber = index;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void _appendResult(LogAnalyzeLineData data) {
|
||||
// 追加结果到 state
|
||||
final currentState = state.value;
|
||||
@ -266,195 +97,4 @@ class ToolsLogAnalyze extends _$ToolsLogAnalyze {
|
||||
state = AsyncData([data]);
|
||||
}
|
||||
}
|
||||
|
||||
final _baseRegExp = RegExp(r'\[Notice\]\s+<([^>]+)>');
|
||||
|
||||
String? _baseEventDecoder(String line) {
|
||||
// 解析 log 行的基本信息
|
||||
final match = _baseRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
final type = match.group(1);
|
||||
return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final _gameLoadingRegExp =
|
||||
RegExp(r'<[^>]+>\s+Loading screen for\s+(\w+)\s+:\s+SC_Frontend closed after\s+(\d+\.\d+)\s+seconds');
|
||||
|
||||
(String, String)? _logGetGameLoading(String line) {
|
||||
final match = _gameLoadingRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
return (match.group(1) ?? "-", match.group(2) ?? "-");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final DateFormat _dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss:SSS');
|
||||
final _logDateTimeRegExp = RegExp(r'<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)>');
|
||||
|
||||
DateTime? _getLogLineDateTime(String line) {
|
||||
// 提取 log 行的时间
|
||||
final match = _logDateTimeRegExp.firstMatch(line);
|
||||
if (match != null) {
|
||||
final dateTimeString = match.group(1);
|
||||
if (dateTimeString != null) {
|
||||
return DateTime.parse(dateTimeString).toLocal();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _getLogLineDateTimeString(String line) {
|
||||
// 提取 log 行的时间
|
||||
final dateTime = _getLogLineDateTime(line);
|
||||
if (dateTime != null) {
|
||||
return _dateTimeFormatter.format(dateTime);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 安全提取函数
|
||||
String? safeExtract(RegExp pattern, String line) => pattern.firstMatch(line)?.group(1)?.trim();
|
||||
|
||||
LogAnalyzeLineData? _logGetFatalCollision(String line) {
|
||||
final patterns = {
|
||||
'zone': RegExp(r'\[Part:[^\]]*?Zone:\s*([^,\]]+)'),
|
||||
'player_pilot': RegExp(r'PlayerPilot:\s*(\d)'),
|
||||
'hit_entity': RegExp(r'hitting entity:\s*(\w+)'),
|
||||
'hit_entity_vehicle': RegExp(r'hitting entity:[^\[]*\[Zone:\s*([^\s-]+)'),
|
||||
'distance': RegExp(r'Distance:\s*([\d.]+)')
|
||||
};
|
||||
|
||||
final zone = safeExtract(patterns['zone']!, line) ?? unknownValue;
|
||||
final playerPilot = (safeExtract(patterns['player_pilot']!, line) ?? '0') == '1';
|
||||
final hitEntity = safeExtract(patterns['hit_entity']!, line) ?? unknownValue;
|
||||
final hitEntityVehicle = safeExtract(patterns['hit_entity_vehicle']!, line) ?? unknownValue;
|
||||
final distance = double.tryParse(safeExtract(patterns['distance']!, line) ?? '') ?? 0.0;
|
||||
return LogAnalyzeLineData(
|
||||
type: "fatal_collision",
|
||||
title: S.current.log_analyzer_filter_fatal_collision,
|
||||
data: S.current.log_analyzer_collision_details(
|
||||
zone,
|
||||
playerPilot ? '✅' : '❌',
|
||||
hitEntity,
|
||||
hitEntityVehicle,
|
||||
distance.toStringAsFixed(2),
|
||||
),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetVehicleDestruction(String line) {
|
||||
final pattern = RegExp(r"Vehicle\s+'([^']+)'.*?" // 载具型号
|
||||
r"in zone\s+'([^']+)'.*?" // Zone
|
||||
r"destroy level \d+ to (\d+).*?" // 损毁等级
|
||||
r"caused by\s+'([^']+)'" // 责任方
|
||||
);
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final vehicleModel = match.group(1) ?? unknownValue;
|
||||
final zone = match.group(2) ?? unknownValue;
|
||||
final destructionLevel = int.tryParse(match.group(3) ?? '') ?? 0;
|
||||
final causedBy = match.group(4) ?? unknownValue;
|
||||
|
||||
final destructionLevelMap = {1: S.current.log_analyzer_soft_death, 2: S.current.log_analyzer_disintegration};
|
||||
|
||||
if (causedBy.trim() == _playerName) {
|
||||
if (destructionLevel == 1) {
|
||||
_vehicleDestructionCount++;
|
||||
} else if (destructionLevel == 2) {
|
||||
_vehicleDestructionCountHard++;
|
||||
}
|
||||
}
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "vehicle_destruction",
|
||||
title: S.current.log_analyzer_filter_vehicle_damaged,
|
||||
data: S.current.log_analyzer_vehicle_damage_details(
|
||||
vehicleModel,
|
||||
zone,
|
||||
destructionLevel.toString(),
|
||||
destructionLevelMap[destructionLevel] ?? unknownValue,
|
||||
causedBy,
|
||||
),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetActorDeath(String line) {
|
||||
final pattern = RegExp(r"CActor::Kill: '([^']+)'.*?" // 受害者ID
|
||||
r"in zone '([^']+)'.*?" // 死亡位置区域
|
||||
r"killed by '([^']+)'.*?" // 击杀者ID
|
||||
r"with damage type '([^']+)'" // 伤害类型
|
||||
);
|
||||
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final victimId = match.group(1) ?? unknownValue;
|
||||
final zone = match.group(2) ?? unknownValue;
|
||||
final killerId = match.group(3) ?? unknownValue;
|
||||
final damageType = match.group(4) ?? unknownValue;
|
||||
|
||||
if (victimId.trim() == killerId.trim()) {
|
||||
// 自杀
|
||||
_selfKillCount++;
|
||||
} else {
|
||||
if (victimId.trim() == _playerName) {
|
||||
_deathCount++;
|
||||
}
|
||||
if (killerId.trim() == _playerName) {
|
||||
_killCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "actor_death",
|
||||
title: S.current.log_analyzer_filter_character_death,
|
||||
data: S.current.log_analyzer_death_details(
|
||||
victimId,
|
||||
damageType,
|
||||
killerId,
|
||||
zone,
|
||||
),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetCharacterName(String line) {
|
||||
final pattern = RegExp(r"name\s+([^-]+)");
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final characterName = match.group(1)?.trim() ?? unknownValue;
|
||||
_playerName = characterName.trim(); // 更新玩家名称
|
||||
return LogAnalyzeLineData(
|
||||
type: "player_login",
|
||||
title: S.current.log_analyzer_player_login(characterName),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LogAnalyzeLineData? _logGetRequestLocationInventory(String line) {
|
||||
final pattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
|
||||
final match = pattern.firstMatch(line);
|
||||
if (match != null) {
|
||||
final playerId = match.group(1) ?? unknownValue;
|
||||
final location = match.group(2) ?? unknownValue;
|
||||
|
||||
return LogAnalyzeLineData(
|
||||
type: "request_location_inventory",
|
||||
title: S.current.log_analyzer_view_local_inventory,
|
||||
data: S.current.log_analyzer_player_location(playerId, location),
|
||||
dateTime: _getLogLineDateTimeString(line),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,280 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'log_analyze_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$LogAnalyzeLineData {
|
||||
|
||||
String get type; String get title; String? get data; String? get dateTime;
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$LogAnalyzeLineDataCopyWith<LogAnalyzeLineData> get copyWith => _$LogAnalyzeLineDataCopyWithImpl<LogAnalyzeLineData>(this as LogAnalyzeLineData, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LogAnalyzeLineData&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.data, data) || other.data == data)&&(identical(other.dateTime, dateTime) || other.dateTime == dateTime));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,title,data,dateTime);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $LogAnalyzeLineDataCopyWith<$Res> {
|
||||
factory $LogAnalyzeLineDataCopyWith(LogAnalyzeLineData value, $Res Function(LogAnalyzeLineData) _then) = _$LogAnalyzeLineDataCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String type, String title, String? data, String? dateTime
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$LogAnalyzeLineDataCopyWithImpl<$Res>
|
||||
implements $LogAnalyzeLineDataCopyWith<$Res> {
|
||||
_$LogAnalyzeLineDataCopyWithImpl(this._self, this._then);
|
||||
|
||||
final LogAnalyzeLineData _self;
|
||||
final $Res Function(LogAnalyzeLineData) _then;
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? title = null,Object? data = freezed,Object? dateTime = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as String?,dateTime: freezed == dateTime ? _self.dateTime : dateTime // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [LogAnalyzeLineData].
|
||||
extension LogAnalyzeLineDataPatterns on LogAnalyzeLineData {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LogAnalyzeLineData value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LogAnalyzeLineData() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LogAnalyzeLineData value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LogAnalyzeLineData():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LogAnalyzeLineData value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _LogAnalyzeLineData() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String title, String? data, String? dateTime)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LogAnalyzeLineData() when $default != null:
|
||||
return $default(_that.type,_that.title,_that.data,_that.dateTime);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String title, String? data, String? dateTime) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LogAnalyzeLineData():
|
||||
return $default(_that.type,_that.title,_that.data,_that.dateTime);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String title, String? data, String? dateTime)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LogAnalyzeLineData() when $default != null:
|
||||
return $default(_that.type,_that.title,_that.data,_that.dateTime);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _LogAnalyzeLineData implements LogAnalyzeLineData {
|
||||
const _LogAnalyzeLineData({required this.type, required this.title, this.data, this.dateTime});
|
||||
|
||||
|
||||
@override final String type;
|
||||
@override final String title;
|
||||
@override final String? data;
|
||||
@override final String? dateTime;
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$LogAnalyzeLineDataCopyWith<_LogAnalyzeLineData> get copyWith => __$LogAnalyzeLineDataCopyWithImpl<_LogAnalyzeLineData>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LogAnalyzeLineData&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.data, data) || other.data == data)&&(identical(other.dateTime, dateTime) || other.dateTime == dateTime));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,title,data,dateTime);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$LogAnalyzeLineDataCopyWith<$Res> implements $LogAnalyzeLineDataCopyWith<$Res> {
|
||||
factory _$LogAnalyzeLineDataCopyWith(_LogAnalyzeLineData value, $Res Function(_LogAnalyzeLineData) _then) = __$LogAnalyzeLineDataCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, String title, String? data, String? dateTime
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$LogAnalyzeLineDataCopyWithImpl<$Res>
|
||||
implements _$LogAnalyzeLineDataCopyWith<$Res> {
|
||||
__$LogAnalyzeLineDataCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _LogAnalyzeLineData _self;
|
||||
final $Res Function(_LogAnalyzeLineData) _then;
|
||||
|
||||
/// Create a copy of LogAnalyzeLineData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? title = null,Object? data = freezed,Object? dateTime = freezed,}) {
|
||||
return _then(_LogAnalyzeLineData(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as String?,dateTime: freezed == dateTime ? _self.dateTime : dateTime // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@ -50,7 +50,7 @@ final class ToolsLogAnalyzeProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$toolsLogAnalyzeHash() => r'5666c3f882e22e2192593629164bc53f8ce4aabe';
|
||||
String _$toolsLogAnalyzeHash() => r'f5079c7d35daf25b07f83bacb224484171e9c93f';
|
||||
|
||||
final class ToolsLogAnalyzeFamily extends $Family
|
||||
with
|
||||
|
||||
@ -29,7 +29,12 @@ ndarray = "0.17"
|
||||
serde_json = "1.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62.2", features = ["Win32_UI_WindowsAndMessaging"] }
|
||||
windows = { version = "0.62.2", features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Foundation"
|
||||
] }
|
||||
win32job = "2"
|
||||
|
||||
[lints.rust]
|
||||
|
||||
@ -49,4 +49,129 @@ pub fn set_foreground_window(window_name: &str) -> anyhow::Result<bool> {
|
||||
pub fn set_foreground_window(window_name: &str) -> anyhow::Result<bool> {
|
||||
println!("set_foreground_window (unix): {}", window_name);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcessInfo {
|
||||
pub pid: u32,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_process_pid_by_name(process_name: &str) -> anyhow::Result<i32> {
|
||||
// 保持向后兼容:返回第一个匹配进程的 PID
|
||||
let processes = get_process_list_by_name(process_name)?;
|
||||
if let Some(first) = processes.first() {
|
||||
Ok(first.pid as i32)
|
||||
} else {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn get_process_pid_by_name(process_name: &str) -> anyhow::Result<i32> {
|
||||
println!("get_process_pid_by_name (unix): {}", process_name);
|
||||
Ok(-1)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_process_list_by_name(process_name: &str) -> anyhow::Result<Vec<ProcessInfo>> {
|
||||
use std::mem;
|
||||
use windows::Win32::Foundation::CloseHandle;
|
||||
use windows::Win32::System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
||||
TH32CS_SNAPPROCESS,
|
||||
};
|
||||
|
||||
let mut result = Vec::new();
|
||||
let search_lower = process_name.to_lowercase();
|
||||
|
||||
unsafe {
|
||||
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)?;
|
||||
if snapshot.is_invalid() {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
let mut process_entry: PROCESSENTRY32W = mem::zeroed();
|
||||
process_entry.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
|
||||
|
||||
if Process32FirstW(snapshot, &mut process_entry).is_err() {
|
||||
let _ = CloseHandle(snapshot);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
loop {
|
||||
// 将 WCHAR 数组转换为 String
|
||||
let exe_file = String::from_utf16_lossy(
|
||||
&process_entry.szExeFile[..process_entry
|
||||
.szExeFile
|
||||
.iter()
|
||||
.position(|&c| c == 0)
|
||||
.unwrap_or(process_entry.szExeFile.len())],
|
||||
);
|
||||
|
||||
// 支持部分匹配(不区分大小写)
|
||||
if exe_file.to_lowercase().contains(&search_lower) {
|
||||
let pid = process_entry.th32ProcessID;
|
||||
|
||||
// 获取完整路径
|
||||
let full_path = get_process_path(pid).unwrap_or_default();
|
||||
|
||||
result.push(ProcessInfo {
|
||||
pid,
|
||||
name: exe_file,
|
||||
path: full_path,
|
||||
});
|
||||
}
|
||||
|
||||
if Process32NextW(snapshot, &mut process_entry).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = CloseHandle(snapshot);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_process_path(pid: u32) -> Option<String> {
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Foundation::{CloseHandle, MAX_PATH};
|
||||
use windows::Win32::System::Threading::{
|
||||
OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
if let Ok(h_process) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
|
||||
if !h_process.is_invalid() {
|
||||
let mut path_buffer = [0u16; MAX_PATH as usize];
|
||||
let mut path_len = path_buffer.len() as u32;
|
||||
|
||||
let result = if QueryFullProcessImageNameW(
|
||||
h_process,
|
||||
PROCESS_NAME_WIN32,
|
||||
PWSTR::from_raw(path_buffer.as_mut_ptr()),
|
||||
&mut path_len,
|
||||
).is_ok() && path_len > 0 {
|
||||
Some(String::from_utf16_lossy(&path_buffer[..path_len as usize]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let _ = CloseHandle(h_process);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn get_process_list_by_name(process_name: &str) -> anyhow::Result<Vec<ProcessInfo>> {
|
||||
println!("get_process_list_by_name (unix): {}", process_name);
|
||||
Ok(Vec::new())
|
||||
}
|
||||
@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueNom,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -706588047;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1227557070;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@ -156,6 +156,54 @@ fn wire__crate__api__http_api__fetch_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__win32_api__get_process_list_by_name_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
process_name: impl CstDecode<String>,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "get_process_list_by_name",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let api_process_name = process_name.cst_decode();
|
||||
move |context| {
|
||||
transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||
(move || {
|
||||
let output_ok =
|
||||
crate::api::win32_api::get_process_list_by_name(&api_process_name)?;
|
||||
Ok(output_ok)
|
||||
})(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__win32_api__get_process_pid_by_name_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
process_name: impl CstDecode<String>,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "get_process_pid_by_name",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let api_process_name = process_name.cst_decode();
|
||||
move |context| {
|
||||
transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||
(move || {
|
||||
let output_ok =
|
||||
crate::api::win32_api::get_process_pid_by_name(&api_process_name)?;
|
||||
Ok(output_ok)
|
||||
})(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__asar_api__get_rsi_launcher_asar_data_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
asar_path: impl CstDecode<String>,
|
||||
@ -627,6 +675,20 @@ impl SseDecode for Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<crate::api::win32_api::ProcessInfo> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut len_ = <i32>::sse_decode(deserializer);
|
||||
let mut ans_ = vec![];
|
||||
for idx_ in 0..len_ {
|
||||
ans_.push(<crate::api::win32_api::ProcessInfo>::sse_decode(
|
||||
deserializer,
|
||||
));
|
||||
}
|
||||
return ans_;
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Vec<(String, String)> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@ -731,6 +793,20 @@ impl SseDecode for Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for crate::api::win32_api::ProcessInfo {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_pid = <u32>::sse_decode(deserializer);
|
||||
let mut var_name = <String>::sse_decode(deserializer);
|
||||
let mut var_path = <String>::sse_decode(deserializer);
|
||||
return crate::api::win32_api::ProcessInfo {
|
||||
pid: var_pid,
|
||||
name: var_name,
|
||||
path: var_path,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for (String, String) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@ -918,6 +994,28 @@ impl flutter_rust_bridge::IntoIntoDart<crate::api::http_api::MyMethod>
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::win32_api::ProcessInfo {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
self.pid.into_into_dart().into_dart(),
|
||||
self.name.into_into_dart().into_dart(),
|
||||
self.path.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
}
|
||||
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||
for crate::api::win32_api::ProcessInfo
|
||||
{
|
||||
}
|
||||
impl flutter_rust_bridge::IntoIntoDart<crate::api::win32_api::ProcessInfo>
|
||||
for crate::api::win32_api::ProcessInfo
|
||||
{
|
||||
fn into_into_dart(self) -> crate::api::win32_api::ProcessInfo {
|
||||
self
|
||||
}
|
||||
}
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
impl flutter_rust_bridge::IntoDart for crate::api::rs_process::RsProcessStreamData {
|
||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||
[
|
||||
@ -1077,6 +1175,16 @@ impl SseEncode for Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<crate::api::win32_api::ProcessInfo> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<i32>::sse_encode(self.len() as _, serializer);
|
||||
for item in self {
|
||||
<crate::api::win32_api::ProcessInfo>::sse_encode(item, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Vec<(String, String)> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@ -1180,6 +1288,15 @@ impl SseEncode for Option<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for crate::api::win32_api::ProcessInfo {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<u32>::sse_encode(self.pid, serializer);
|
||||
<String>::sse_encode(self.name, serializer);
|
||||
<String>::sse_encode(self.path, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for (String, String) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
@ -1379,6 +1496,16 @@ mod io {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CstDecode<Vec<crate::api::win32_api::ProcessInfo>> for *mut wire_cst_list_process_info {
|
||||
// Codec=Cst (C-struct based), see doc to use other codecs
|
||||
fn cst_decode(self) -> Vec<crate::api::win32_api::ProcessInfo> {
|
||||
let vec = unsafe {
|
||||
let wrap = flutter_rust_bridge::for_generated::box_from_leak_ptr(self);
|
||||
flutter_rust_bridge::for_generated::vec_from_leak_ptr(wrap.ptr, wrap.len)
|
||||
};
|
||||
vec.into_iter().map(CstDecode::cst_decode).collect()
|
||||
}
|
||||
}
|
||||
impl CstDecode<Vec<(String, String)>> for *mut wire_cst_list_record_string_string {
|
||||
// Codec=Cst (C-struct based), see doc to use other codecs
|
||||
fn cst_decode(self) -> Vec<(String, String)> {
|
||||
@ -1389,6 +1516,16 @@ mod io {
|
||||
vec.into_iter().map(CstDecode::cst_decode).collect()
|
||||
}
|
||||
}
|
||||
impl CstDecode<crate::api::win32_api::ProcessInfo> for wire_cst_process_info {
|
||||
// Codec=Cst (C-struct based), see doc to use other codecs
|
||||
fn cst_decode(self) -> crate::api::win32_api::ProcessInfo {
|
||||
crate::api::win32_api::ProcessInfo {
|
||||
pid: self.pid.cst_decode(),
|
||||
name: self.name.cst_decode(),
|
||||
path: self.path.cst_decode(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CstDecode<(String, String)> for wire_cst_record_string_string {
|
||||
// Codec=Cst (C-struct based), see doc to use other codecs
|
||||
fn cst_decode(self) -> (String, String) {
|
||||
@ -1429,6 +1566,20 @@ mod io {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl NewWithNullPtr for wire_cst_process_info {
|
||||
fn new_with_null_ptr() -> Self {
|
||||
Self {
|
||||
pid: Default::default(),
|
||||
name: core::ptr::null_mut(),
|
||||
path: core::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for wire_cst_process_info {
|
||||
fn default() -> Self {
|
||||
Self::new_with_null_ptr()
|
||||
}
|
||||
}
|
||||
impl NewWithNullPtr for wire_cst_record_string_string {
|
||||
fn new_with_null_ptr() -> Self {
|
||||
Self {
|
||||
@ -1533,6 +1684,22 @@ mod io {
|
||||
)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__get_process_list_by_name(
|
||||
port_: i64,
|
||||
process_name: *mut wire_cst_list_prim_u_8_strict,
|
||||
) {
|
||||
wire__crate__api__win32_api__get_process_list_by_name_impl(port_, process_name)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__get_process_pid_by_name(
|
||||
port_: i64,
|
||||
process_name: *mut wire_cst_list_prim_u_8_strict,
|
||||
) {
|
||||
wire__crate__api__win32_api__get_process_pid_by_name_impl(port_, process_name)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__asar_api__get_rsi_launcher_asar_data(
|
||||
port_: i64,
|
||||
@ -1698,6 +1865,20 @@ mod io {
|
||||
flutter_rust_bridge::for_generated::new_leak_box_ptr(ans)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn frbgen_starcitizen_doctor_cst_new_list_process_info(
|
||||
len: i32,
|
||||
) -> *mut wire_cst_list_process_info {
|
||||
let wrap = wire_cst_list_process_info {
|
||||
ptr: flutter_rust_bridge::for_generated::new_leak_vec_ptr(
|
||||
<wire_cst_process_info>::new_with_null_ptr(),
|
||||
len,
|
||||
),
|
||||
len,
|
||||
};
|
||||
flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn frbgen_starcitizen_doctor_cst_new_list_record_string_string(
|
||||
len: i32,
|
||||
@ -1732,12 +1913,25 @@ mod io {
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct wire_cst_list_process_info {
|
||||
ptr: *mut wire_cst_process_info,
|
||||
len: i32,
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct wire_cst_list_record_string_string {
|
||||
ptr: *mut wire_cst_record_string_string,
|
||||
len: i32,
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct wire_cst_process_info {
|
||||
pid: u32,
|
||||
name: *mut wire_cst_list_prim_u_8_strict,
|
||||
path: *mut wire_cst_list_prim_u_8_strict,
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct wire_cst_record_string_string {
|
||||
field0: *mut wire_cst_list_prim_u_8_strict,
|
||||
field1: *mut wire_cst_list_prim_u_8_strict,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user