diff --git a/lib/common/helper/game_log_analyzer.dart b/lib/common/helper/game_log_analyzer.dart new file mode 100644 index 0000000..881da31 --- /dev/null +++ b/lib/common/helper/game_log_analyzer.dart @@ -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 = ""; + + // 正则表达式定义 + 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, 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, LogAnalyzeStatistics) _analyzeLogLines( + List logLines, { + DateTime? startTime, + }) { + final results = []; + 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; + } +} diff --git a/lib/common/rust/api/win32_api.dart b/lib/common/rust/api/win32_api.dart index e0f4ccd..78d9de7 100644 --- a/lib/common/rust/api/win32_api.dart +++ b/lib/common/rust/api/win32_api.dart @@ -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 sendNotify({ String? summary, String? body, @@ -22,3 +25,37 @@ Future setForegroundWindow({required String windowName}) => RustLib .instance .api .crateApiWin32ApiSetForegroundWindow(windowName: windowName); + +Future getProcessPidByName({required String processName}) => RustLib + .instance + .api + .crateApiWin32ApiGetProcessPidByName(processName: processName); + +Future> 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; +} diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index 5cbea7e..3bdc0d7 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint { 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> crateApiWin32ApiGetProcessListByName({ + required String processName, + }); + + Future crateApiWin32ApiGetProcessPidByName({ + required String processName, + }); + Future crateApiAsarApiGetRsiLauncherAsarData({ required String asarPath, }); @@ -280,6 +288,66 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ], ); + @override + Future> 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 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 crateApiAsarApiGetRsiLauncherAsarData({ required String asarPath, @@ -722,6 +790,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as Uint8List; } + @protected + List dco_decode_list_process_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).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; + 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 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_ = []; + 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 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, diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index c03e956..49e40fa 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -62,6 +62,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + @protected + List 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 { @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 { @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + List 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 { @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 { return ans; } + @protected + ffi.Pointer cst_encode_list_process_info( + List 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 cst_encode_list_record_string_string(List<(String, String)> raw) { @@ -374,6 +398,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { 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 { SseSerializer serializer, ); + @protected + void sse_encode_list_process_info( + List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_record_string_string( List<(String, String)> self, @@ -532,6 +572,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { 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 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, + ) + > + >( + '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) + >(); + + void wire__crate__api__win32_api__get_process_pid_by_name( + int port_, + ffi.Pointer 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, + ) + > + >( + '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) + >(); + void wire__crate__api__asar_api__get_rsi_launcher_asar_data( int port_, ffi.Pointer 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 Function(int)>(); + ffi.Pointer 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 Function(ffi.Int32) + > + >('frbgen_starcitizen_doctor_cst_new_list_process_info'); + late final _cst_new_list_process_info = _cst_new_list_process_infoPtr + .asFunction Function(int)>(); + ffi.Pointer 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 name; + + external ffi.Pointer path; +} + +final class wire_cst_list_process_info extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_rs_process_stream_data extends ffi.Struct { @ffi.Int32() external int data_type; diff --git a/lib/ui/party_room/party_room_ui.dart b/lib/ui/party_room/party_room_ui.dart index bc19a47..bc63f08 100644 --- a/lib/ui/party_room/party_room_ui.dart +++ b/lib/ui/party_room/party_room_ui.dart @@ -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 diff --git a/lib/ui/party_room/party_room_ui_model.dart b/lib/ui/party_room/party_room_ui_model.dart index f782256..0bb398f 100644 --- a/lib/ui/party_room/party_room_ui_model.dart +++ b/lib/ui/party_room/party_room_ui_model.dart @@ -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( + 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); } diff --git a/lib/ui/party_room/party_room_ui_model.g.dart b/lib/ui/party_room/party_room_ui_model.g.dart index 449df70..ff73367 100644 --- a/lib/ui/party_room/party_room_ui_model.g.dart +++ b/lib/ui/party_room/party_room_ui_model.g.dart @@ -41,7 +41,7 @@ final class PartyRoomUIModelProvider } } -String _$partyRoomUIModelHash() => r'48291373cafc9005843478a90970152426b3a666'; +String _$partyRoomUIModelHash() => r'a0b6c3632ff33f2d58882f9bc1ab58c69c2487f4'; abstract class _$PartyRoomUIModel extends $Notifier { PartyRoomUIState build(); diff --git a/lib/ui/party_room/utils/game_log_tracker_provider.dart b/lib/ui/party_room/utils/game_log_tracker_provider.dart new file mode 100644 index 0000000..6b21537 --- /dev/null +++ b/lib/ui/party_room/utils/game_log_tracker_provider.dart @@ -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 killedIds, // 本次迭代新增的击杀ID + @Default([]) List 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 _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: '', gameStartTime: null); + } + } catch (e) { + // 游戏未启动或发生错误 + state = state.copyWith( + location: '<游戏未启动>', + gameStartTime: null, + kills: 0, + deaths: 0, + killedIds: [], + deathIds: [], + ); + } + await Future.delayed(const Duration(seconds: 5)); + } + } + + Future _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 = []; + final newDeathIds = []; + + 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"); + } +} diff --git a/lib/ui/party_room/utils/game_log_tracker_provider.freezed.dart b/lib/ui/party_room/utils/game_log_tracker_provider.freezed.dart new file mode 100644 index 0000000..0b16234 --- /dev/null +++ b/lib/ui/party_room/utils/game_log_tracker_provider.freezed.dart @@ -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 value) => value; +/// @nodoc +mixin _$PartyRoomGameLogTrackerProviderState { + + String get location; int get kills; int get deaths; DateTime? get gameStartTime; List get killedIds;// 本次迭代新增的击杀ID + List 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 get copyWith => _$PartyRoomGameLogTrackerProviderStateCopyWithImpl(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 killedIds, List 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,deathIds: null == deathIds ? _self.deathIds : deathIds // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// 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 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 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? 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 Function( String location, int kills, int deaths, DateTime? gameStartTime, List killedIds, List 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 Function( String location, int kills, int deaths, DateTime? gameStartTime, List killedIds, List 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? Function( String location, int kills, int deaths, DateTime? gameStartTime, List killedIds, List 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 killedIds = const [], final List 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 _killedIds; +@override@JsonKey() List get killedIds { + if (_killedIds is EqualUnmodifiableListView) return _killedIds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_killedIds); +} + +// 本次迭代新增的击杀ID + final List _deathIds; +// 本次迭代新增的击杀ID +@override@JsonKey() List 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 killedIds, List 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,deathIds: null == deathIds ? _self._deathIds : deathIds // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/ui/party_room/utils/game_log_tracker_provider.g.dart b/lib/ui/party_room/utils/game_log_tracker_provider.g.dart new file mode 100644 index 0000000..7547c9f --- /dev/null +++ b/lib/ui/party_room/utils/game_log_tracker_provider.g.dart @@ -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(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 { + 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); + } +} diff --git a/lib/ui/party_room/widgets/detail/party_room_header.dart b/lib/ui/party_room/widgets/detail/party_room_header.dart index 04e2f3a..6f11aad 100644 --- a/lib/ui/party_room/widgets/detail/party_room_header.dart +++ b/lib/ui/party_room/widgets/detail/party_room_header.dart @@ -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); }, diff --git a/lib/ui/party_room/widgets/detail/party_room_member_list.dart b/lib/ui/party_room/widgets/detail/party_room_member_list.dart index 7c539b0..b339d1d 100644 --- a/lib/ui/party_room/widgets/detail/party_room_member_list.dart +++ b/lib/ui/party_room/widgets/detail/party_room_member_list.dart @@ -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)); }, diff --git a/lib/ui/party_room/widgets/detail/party_room_message_list.dart b/lib/ui/party_room/widgets/detail/party_room_message_list.dart index d275c95..83ac2ae 100644 --- a/lib/ui/party_room/widgets/detail/party_room_message_list.dart +++ b/lib/ui/party_room/widgets/detail/party_room_message_list.dart @@ -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; } } diff --git a/lib/ui/tools/log_analyze_ui/log_analyze_provider.dart b/lib/ui/tools/log_analyze_ui/log_analyze_provider.dart index 96d637e..42c97d8 100644 --- a/lib/ui/tools/log_analyze_ui/log_analyze_provider.dart +++ b/lib/ui/tools/log_analyze_ui/log_analyze_provider.dart @@ -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 logAnalyzeSearchTypeMap = { null: S.current.log_analyzer_filter_all, "info": S.current.log_analyzer_filter_basic_info, @@ -26,132 +21,29 @@ final Map 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 = ""; - @override Future> 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; - } } diff --git a/lib/ui/tools/log_analyze_ui/log_analyze_provider.freezed.dart b/lib/ui/tools/log_analyze_ui/log_analyze_provider.freezed.dart deleted file mode 100644 index 7815d63..0000000 --- a/lib/ui/tools/log_analyze_ui/log_analyze_provider.freezed.dart +++ /dev/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 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 get copyWith => _$LogAnalyzeLineDataCopyWithImpl(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 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 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? 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 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 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? 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 diff --git a/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart b/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart index 9ca1959..bcb5d08 100644 --- a/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart +++ b/lib/ui/tools/log_analyze_ui/log_analyze_provider.g.dart @@ -50,7 +50,7 @@ final class ToolsLogAnalyzeProvider } } -String _$toolsLogAnalyzeHash() => r'5666c3f882e22e2192593629164bc53f8ce4aabe'; +String _$toolsLogAnalyzeHash() => r'f5079c7d35daf25b07f83bacb224484171e9c93f'; final class ToolsLogAnalyzeFamily extends $Family with diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 204095f..d784f6d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -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] diff --git a/rust/src/api/win32_api.rs b/rust/src/api/win32_api.rs index aea7837..338be7b 100644 --- a/rust/src/api/win32_api.rs +++ b/rust/src/api/win32_api.rs @@ -49,4 +49,129 @@ pub fn set_foreground_window(window_name: &str) -> anyhow::Result { pub fn set_foreground_window(window_name: &str) -> anyhow::Result { 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 { + // 保持向后兼容:返回第一个匹配进程的 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 { + 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> { + 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::() 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 { + 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> { + println!("get_process_list_by_name (unix): {}", process_name); + Ok(Vec::new()) } \ No newline at end of file diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 6e2f10b..3c763f0 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -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, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + 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, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + 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, @@ -627,6 +675,20 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec { + // 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_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::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> { } } +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 = ::sse_decode(deserializer); + let mut var_name = ::sse_decode(deserializer); + let mut var_path = ::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 } } // 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 + 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 { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::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> { } } +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) { + ::sse_encode(self.pid, serializer); + ::sse_encode(self.name, serializer); + ::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> for *mut wire_cst_list_process_info { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> Vec { + 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> 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 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( + ::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,