feat: 增加基础的游戏进程监听

This commit is contained in:
xkeyC
2025-11-20 00:27:20 +08:00
parent f6340337db
commit b65187d4f0
19 changed files with 1873 additions and 702 deletions

View File

@@ -0,0 +1,172 @@
import 'dart:io';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
import 'package:starcitizen_doctor/common/rust/api/win32_api.dart' as win32;
import 'package:starcitizen_doctor/common/utils/log.dart';
part 'game_log_tracker_provider.freezed.dart';
part 'game_log_tracker_provider.g.dart';
@freezed
sealed class PartyRoomGameLogTrackerProviderState with _$PartyRoomGameLogTrackerProviderState {
const factory PartyRoomGameLogTrackerProviderState({
@Default('') String location,
@Default(0) int kills,
@Default(0) int deaths,
DateTime? gameStartTime,
@Default([]) List<String> killedIds, // 本次迭代新增的击杀ID
@Default([]) List<String> deathIds, // 本次迭代新增的死亡ID
}) = _PartyRoomGameLogTrackerProviderState;
}
@riverpod
class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider {
var _disposed = false;
// 记录上次查询的时间点,用于计算增量
DateTime? _lastQueryTime;
@override
PartyRoomGameLogTrackerProviderState build({required DateTime startTime}) {
dPrint("[PartyRoomGameLogTrackerProvider] init $startTime");
ref.onDispose(() {
_disposed = true;
dPrint("[PartyRoomGameLogTrackerProvider] disposed $startTime");
});
_lastQueryTime = startTime;
_listenToGameLogs(startTime);
return const PartyRoomGameLogTrackerProviderState();
}
Future<void> _listenToGameLogs(DateTime startTime) async {
await Future.delayed(const Duration(seconds: 1));
while (!_disposed) {
try {
// 获取正在运行的游戏进程
final l = await win32.getProcessListByName(processName: "StarCitizen.exe");
final p = l
.where((e) => e.path.toLowerCase().contains("starcitizen") && e.path.toLowerCase().contains("bin64"))
.firstOrNull;
if (p == null) throw Exception("process not found");
final logPath = _getLogPath(p);
final logFile = File(logPath);
if (await logFile.exists()) {
// 分析日志文件
await _analyzeLog(logFile, startTime);
} else {
state = state.copyWith(location: '<Unknown>', gameStartTime: null);
}
} catch (e) {
// 游戏未启动或发生错误
state = state.copyWith(
location: '<游戏未启动>',
gameStartTime: null,
kills: 0,
deaths: 0,
killedIds: [],
deathIds: [],
);
}
await Future.delayed(const Duration(seconds: 5));
}
}
Future<void> _analyzeLog(File logFile, DateTime startTime) async {
try {
final now = DateTime.now();
// 使用 GameLogAnalyzer 分析日志
// startTime 只影响计数统计
final result = await GameLogAnalyzer.analyzeLogFile(logFile, startTime: startTime);
final (logData, statistics) = result;
// 从统计数据中直接获取最新位置(全量查找的结果)
final location = statistics.latestLocation == null ? '<主菜单>' : '[${statistics.latestLocation}]';
// 计算基于 _lastQueryTime 之后的增量 ID
final newKilledIds = <String>[];
final newDeathIds = <String>[];
if (_lastQueryTime != null) {
// 遍历所有 actor_death 事件
for (final data in logData) {
if (data.type != "actor_death") continue;
// 解析事件时间
if (data.dateTime != null) {
try {
// 日志时间格式: "yyyy-MM-dd HH:mm:ss:SSS"
// 转换为 ISO 8601 格式再解析
final parts = data.dateTime!.split(' ');
if (parts.length >= 2) {
final datePart = parts[0]; // yyyy-MM-dd
final timeParts = parts[1].split(':');
if (timeParts.length >= 3) {
final hour = timeParts[0];
final minute = timeParts[1];
final secondMillis = timeParts[2]; // ss:SSS 或 ss.SSS
final timeStr = '$datePart $hour:$minute:${secondMillis.replaceAll(':', '.')}';
final eventTime = DateTime.parse(timeStr);
// 只处理在 _lastQueryTime 之后的事件
if (eventTime.isBefore(_lastQueryTime!)) continue;
}
}
} catch (e) {
dPrint("[PartyRoomGameLogTrackerProvider] Failed to parse dateTime: ${data.dateTime}, error: $e");
// 时间解析失败,继续处理该事件(保守策略)
}
}
// 使用格式化字段,不再重新解析
final victimId = data.victimId;
final killerId = data.killerId;
if (victimId != null && killerId != null && victimId != killerId) {
// 如果玩家是击杀者记录被击杀的ID
if (killerId == statistics.playerName) {
newKilledIds.add(victimId);
}
// 如果玩家是受害者记录击杀者ID
if (victimId == statistics.playerName) {
newDeathIds.add(killerId);
}
}
}
}
// 更新状态,只存储本次迭代的增量数据
state = state.copyWith(
location: location,
kills: statistics.killCount,
// 从 startTime 开始的总计数
deaths: statistics.deathCount,
// 从 startTime 开始的总计数
gameStartTime: statistics.gameStartTime,
// 全量查找的游戏开始时间
killedIds: newKilledIds,
// 只存储本次迭代的增量
deathIds: newDeathIds, // 只存储本次迭代的增量
);
// 更新查询时间为本次查询的时刻
_lastQueryTime = now;
} catch (e, stackTrace) {
dPrint("[PartyRoomGameLogTrackerProvider] Error analyzing log: $e");
dPrint("[PartyRoomGameLogTrackerProvider] Stack trace: $stackTrace");
}
}
String _getLogPath(win32.ProcessInfo p) {
var path = p.path;
return path.replaceAll(r"Bin64\StarCitizen.exe", "Game.log");
}
}

View File

@@ -0,0 +1,295 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'game_log_tracker_provider.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PartyRoomGameLogTrackerProviderState {
String get location; int get kills; int get deaths; DateTime? get gameStartTime; List<String> get killedIds;// 本次迭代新增的击杀ID
List<String> get deathIds;
/// Create a copy of PartyRoomGameLogTrackerProviderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PartyRoomGameLogTrackerProviderStateCopyWith<PartyRoomGameLogTrackerProviderState> get copyWith => _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<PartyRoomGameLogTrackerProviderState>(this as PartyRoomGameLogTrackerProviderState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PartyRoomGameLogTrackerProviderState&&(identical(other.location, location) || other.location == location)&&(identical(other.kills, kills) || other.kills == kills)&&(identical(other.deaths, deaths) || other.deaths == deaths)&&(identical(other.gameStartTime, gameStartTime) || other.gameStartTime == gameStartTime)&&const DeepCollectionEquality().equals(other.killedIds, killedIds)&&const DeepCollectionEquality().equals(other.deathIds, deathIds));
}
@override
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(killedIds),const DeepCollectionEquality().hash(deathIds));
@override
String toString() {
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
}
}
/// @nodoc
abstract mixin class $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
factory $PartyRoomGameLogTrackerProviderStateCopyWith(PartyRoomGameLogTrackerProviderState value, $Res Function(PartyRoomGameLogTrackerProviderState) _then) = _$PartyRoomGameLogTrackerProviderStateCopyWithImpl;
@useResult
$Res call({
String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds
});
}
/// @nodoc
class _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
implements $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
_$PartyRoomGameLogTrackerProviderStateCopyWithImpl(this._self, this._then);
final PartyRoomGameLogTrackerProviderState _self;
final $Res Function(PartyRoomGameLogTrackerProviderState) _then;
/// Create a copy of PartyRoomGameLogTrackerProviderState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? killedIds = null,Object? deathIds = null,}) {
return _then(_self.copyWith(
location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String,kills: null == kills ? _self.kills : kills // ignore: cast_nullable_to_non_nullable
as int,deaths: null == deaths ? _self.deaths : deaths // ignore: cast_nullable_to_non_nullable
as int,gameStartTime: freezed == gameStartTime ? _self.gameStartTime : gameStartTime // ignore: cast_nullable_to_non_nullable
as DateTime?,killedIds: null == killedIds ? _self.killedIds : killedIds // ignore: cast_nullable_to_non_nullable
as List<String>,deathIds: null == deathIds ? _self.deathIds : deathIds // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// Adds pattern-matching-related methods to [PartyRoomGameLogTrackerProviderState].
extension PartyRoomGameLogTrackerProviderStatePatterns on PartyRoomGameLogTrackerProviderState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PartyRoomGameLogTrackerProviderState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _PartyRoomGameLogTrackerProviderState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PartyRoomGameLogTrackerProviderState value) $default,){
final _that = this;
switch (_that) {
case _PartyRoomGameLogTrackerProviderState():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PartyRoomGameLogTrackerProviderState value)? $default,){
final _that = this;
switch (_that) {
case _PartyRoomGameLogTrackerProviderState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PartyRoomGameLogTrackerProviderState() when $default != null:
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds) $default,) {final _that = this;
switch (_that) {
case _PartyRoomGameLogTrackerProviderState():
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds)? $default,) {final _that = this;
switch (_that) {
case _PartyRoomGameLogTrackerProviderState() when $default != null:
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);case _:
return null;
}
}
}
/// @nodoc
class _PartyRoomGameLogTrackerProviderState implements PartyRoomGameLogTrackerProviderState {
const _PartyRoomGameLogTrackerProviderState({this.location = '', this.kills = 0, this.deaths = 0, this.gameStartTime, final List<String> killedIds = const [], final List<String> deathIds = const []}): _killedIds = killedIds,_deathIds = deathIds;
@override@JsonKey() final String location;
@override@JsonKey() final int kills;
@override@JsonKey() final int deaths;
@override final DateTime? gameStartTime;
final List<String> _killedIds;
@override@JsonKey() List<String> get killedIds {
if (_killedIds is EqualUnmodifiableListView) return _killedIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_killedIds);
}
// 本次迭代新增的击杀ID
final List<String> _deathIds;
// 本次迭代新增的击杀ID
@override@JsonKey() List<String> get deathIds {
if (_deathIds is EqualUnmodifiableListView) return _deathIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_deathIds);
}
/// Create a copy of PartyRoomGameLogTrackerProviderState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PartyRoomGameLogTrackerProviderStateCopyWith<_PartyRoomGameLogTrackerProviderState> get copyWith => __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<_PartyRoomGameLogTrackerProviderState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PartyRoomGameLogTrackerProviderState&&(identical(other.location, location) || other.location == location)&&(identical(other.kills, kills) || other.kills == kills)&&(identical(other.deaths, deaths) || other.deaths == deaths)&&(identical(other.gameStartTime, gameStartTime) || other.gameStartTime == gameStartTime)&&const DeepCollectionEquality().equals(other._killedIds, _killedIds)&&const DeepCollectionEquality().equals(other._deathIds, _deathIds));
}
@override
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(_killedIds),const DeepCollectionEquality().hash(_deathIds));
@override
String toString() {
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
}
}
/// @nodoc
abstract mixin class _$PartyRoomGameLogTrackerProviderStateCopyWith<$Res> implements $PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
factory _$PartyRoomGameLogTrackerProviderStateCopyWith(_PartyRoomGameLogTrackerProviderState value, $Res Function(_PartyRoomGameLogTrackerProviderState) _then) = __$PartyRoomGameLogTrackerProviderStateCopyWithImpl;
@override @useResult
$Res call({
String location, int kills, int deaths, DateTime? gameStartTime, List<String> killedIds, List<String> deathIds
});
}
/// @nodoc
class __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
implements _$PartyRoomGameLogTrackerProviderStateCopyWith<$Res> {
__$PartyRoomGameLogTrackerProviderStateCopyWithImpl(this._self, this._then);
final _PartyRoomGameLogTrackerProviderState _self;
final $Res Function(_PartyRoomGameLogTrackerProviderState) _then;
/// Create a copy of PartyRoomGameLogTrackerProviderState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? killedIds = null,Object? deathIds = null,}) {
return _then(_PartyRoomGameLogTrackerProviderState(
location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
as String,kills: null == kills ? _self.kills : kills // ignore: cast_nullable_to_non_nullable
as int,deaths: null == deaths ? _self.deaths : deaths // ignore: cast_nullable_to_non_nullable
as int,gameStartTime: freezed == gameStartTime ? _self.gameStartTime : gameStartTime // ignore: cast_nullable_to_non_nullable
as DateTime?,killedIds: null == killedIds ? _self._killedIds : killedIds // ignore: cast_nullable_to_non_nullable
as List<String>,deathIds: null == deathIds ? _self._deathIds : deathIds // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
// dart format on

View File

@@ -0,0 +1,128 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_log_tracker_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(PartyRoomGameLogTrackerProvider)
const partyRoomGameLogTrackerProviderProvider =
PartyRoomGameLogTrackerProviderFamily._();
final class PartyRoomGameLogTrackerProviderProvider
extends
$NotifierProvider<
PartyRoomGameLogTrackerProvider,
PartyRoomGameLogTrackerProviderState
> {
const PartyRoomGameLogTrackerProviderProvider._({
required PartyRoomGameLogTrackerProviderFamily super.from,
required DateTime super.argument,
}) : super(
retry: null,
name: r'partyRoomGameLogTrackerProviderProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$partyRoomGameLogTrackerProviderHash();
@override
String toString() {
return r'partyRoomGameLogTrackerProviderProvider'
''
'($argument)';
}
@$internal
@override
PartyRoomGameLogTrackerProvider create() => PartyRoomGameLogTrackerProvider();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PartyRoomGameLogTrackerProviderState value) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<PartyRoomGameLogTrackerProviderState>(value),
);
}
@override
bool operator ==(Object other) {
return other is PartyRoomGameLogTrackerProviderProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$partyRoomGameLogTrackerProviderHash() =>
r'ecb015eb46d25bfe11bbb153242fd5c4f20ef367';
final class PartyRoomGameLogTrackerProviderFamily extends $Family
with
$ClassFamilyOverride<
PartyRoomGameLogTrackerProvider,
PartyRoomGameLogTrackerProviderState,
PartyRoomGameLogTrackerProviderState,
PartyRoomGameLogTrackerProviderState,
DateTime
> {
const PartyRoomGameLogTrackerProviderFamily._()
: super(
retry: null,
name: r'partyRoomGameLogTrackerProviderProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
PartyRoomGameLogTrackerProviderProvider call({required DateTime startTime}) =>
PartyRoomGameLogTrackerProviderProvider._(
argument: startTime,
from: this,
);
@override
String toString() => r'partyRoomGameLogTrackerProviderProvider';
}
abstract class _$PartyRoomGameLogTrackerProvider
extends $Notifier<PartyRoomGameLogTrackerProviderState> {
late final _$args = ref.$arg as DateTime;
DateTime get startTime => _$args;
PartyRoomGameLogTrackerProviderState build({required DateTime startTime});
@$mustCallSuper
@override
void runBuild() {
final created = build(startTime: _$args);
final ref =
this.ref
as $Ref<
PartyRoomGameLogTrackerProviderState,
PartyRoomGameLogTrackerProviderState
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
PartyRoomGameLogTrackerProviderState,
PartyRoomGameLogTrackerProviderState
>,
PartyRoomGameLogTrackerProviderState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}