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

@@ -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

View File

@@ -1,10 +1,15 @@
import 'dart:async';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart';
import 'package:starcitizen_doctor/provider/party_room.dart';
import 'package:starcitizen_doctor/ui/party_room/utils/party_room_utils.dart' show PartyRoomUtils;
import 'utils/game_log_tracker_provider.dart';
part 'party_room_ui_model.freezed.dart';
@@ -37,6 +42,7 @@ sealed class PartyRoomUIState with _$PartyRoomUIState {
@riverpod
class PartyRoomUIModel extends _$PartyRoomUIModel {
Timer? _reconnectTimer;
ProviderSubscription? _gameLogSubscription;
@override
PartyRoomUIState build() {
@@ -48,18 +54,59 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
if (previous?.room.isInRoom == true && !next.room.isInRoom) {
state = state.copyWith(isMinimized: false);
}
// 监听房间创建时间变化,设置游戏日志监听
if (previous?.room.currentRoom?.createdAt != next.room.currentRoom?.createdAt) {
_setupGameLogListener(next.room.currentRoom?.createdAt);
}
});
connectToServer();
// 在 dispose 时清理定时器
// 在 dispose 时清理定时器和订阅
ref.onDispose(() {
_reconnectTimer?.cancel();
_gameLogSubscription?.close();
});
return state;
}
/// 设置游戏日志监听
void _setupGameLogListener(Int64? createdAt) {
// 清除之前的订阅
_gameLogSubscription?.close();
_gameLogSubscription = null;
final dateTime = PartyRoomUtils.getDateTime(createdAt);
if (dateTime != null) {
_gameLogSubscription = ref.listen<PartyRoomGameLogTrackerProviderState>(
partyRoomGameLogTrackerProviderProvider(startTime: dateTime),
(previous, next) => _onUpdateGameStatus(previous, next),
);
}
}
/// 处理游戏状态更新
void _onUpdateGameStatus(PartyRoomGameLogTrackerProviderState? previous, PartyRoomGameLogTrackerProviderState next) {
// 防抖
final currentGameStartTime = previous?.gameStartTime?.millisecondsSinceEpoch;
final gameStartTime = next.gameStartTime?.microsecondsSinceEpoch;
if (next.kills != previous?.kills ||
next.deaths != previous?.deaths ||
next.location != previous?.location ||
currentGameStartTime != gameStartTime) {
// 更新状态
ref
.read(partyRoomProvider.notifier)
.setStatus(
kills: next.kills != previous?.kills ? next.kills : null,
deaths: next.deaths != previous?.deaths ? next.deaths : null,
currentLocation: next.location != previous?.location ? next.location : null,
playTime: currentGameStartTime != gameStartTime ? gameStartTime : null,
);
}
}
/// 处理连接状态变化
void _handleConnectionStateChange(PartyRoomFullState? previous, PartyRoomFullState next) {
// 检测断线:之前已连接但现在未连接
@@ -140,17 +187,16 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
// 加载标签(游客和登录用户都需要)
await partyRoom.loadTags();
// 非游客模式:尝试登录
try {
state = state.copyWith(isLoggingIn: true);
await partyRoom.login();
// 登录成功,加载房间列表
await loadRoomList();
state = state.copyWith(showRoomList: true);
state = state.copyWith(showRoomList: true, isLoggingIn: false, isGuestMode: false);
} catch (e) {
// 未注册,保持在连接状态
dPrint('[PartyRoomUI] Login failed, need register: $e');
state = state.copyWith(isGuestMode: true);
} finally {
state = state.copyWith(isLoggingIn: false);
}

View File

@@ -41,7 +41,7 @@ final class PartyRoomUIModelProvider
}
}
String _$partyRoomUIModelHash() => r'48291373cafc9005843478a90970152426b3a666';
String _$partyRoomUIModelHash() => r'a0b6c3632ff33f2d58882f9bc1ab58c69c2487f4';
abstract class _$PartyRoomUIModel extends $Notifier<PartyRoomUIState> {
PartyRoomUIState build();

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);
}
}

View File

@@ -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);
},

View File

@@ -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));
},

View File

@@ -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;
}
}

View File

@@ -1,19 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' show debugPrint;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart';
import 'package:starcitizen_doctor/generated/l10n.dart';
import 'package:watcher/watcher.dart';
part 'log_analyze_provider.g.dart';
part 'log_analyze_provider.freezed.dart';
final Map<String?, String> logAnalyzeSearchTypeMap = {
null: S.current.log_analyzer_filter_all,
"info": S.current.log_analyzer_filter_basic_info,
@@ -26,132 +21,29 @@ final Map<String?, String> logAnalyzeSearchTypeMap = {
"request_location_inventory": S.current.log_analyzer_filter_local_inventory,
};
@freezed
abstract class LogAnalyzeLineData with _$LogAnalyzeLineData {
const factory LogAnalyzeLineData({
required String type,
required String title,
String? data,
String? dateTime,
}) = _LogAnalyzeLineData;
}
@riverpod
class ToolsLogAnalyze extends _$ToolsLogAnalyze {
static const String unknownValue = "<Unknown>";
@override
Future<List<LogAnalyzeLineData>> build(String gameInstallPath, bool listSortReverse) async {
final logFile = File("$gameInstallPath/Game.log");
debugPrint("[ToolsLogAnalyze] logFile: ${logFile.absolute.path}");
if (gameInstallPath.isEmpty || !(await logFile.exists())) {
return [
LogAnalyzeLineData(
type: "error",
title: S.current.log_analyzer_no_log_file,
)
];
return [const LogAnalyzeLineData(type: "error", title: "未找到日志文件")];
}
state = AsyncData([]);
state = const AsyncData([]);
_launchLogAnalyze(logFile);
return state.value ?? [];
}
String _playerName = ""; // 记录玩家名称
int _killCount = 0; // 记录击杀其他实体次数
int _deathCount = 0; // 记录被击杀次数
int _selfKillCount = 0; // 记录自杀次数
int _vehicleDestructionCount = 0; // 记录载具损毁次数 (软死亡)
int _vehicleDestructionCountHard = 0; // 记录载具损毁次数 (解体)
DateTime? _gameStartTime; // 记录游戏开始时间
int _gameCrashLineNumber = -1; // 记录${S.current.log_analyzer_filter_game_crash}行号
int _currentLineNumber = 0; // 当前行号
void _launchLogAnalyze(File logFile) async {
// 使用新的 GameLogAnalyzer 工具类
final result = await GameLogAnalyzer.analyzeLogFile(logFile);
final (results, _) = result;
void _launchLogAnalyze(File logFile, {int startLine = 0}) async {
final logLines = utf8.decode((await logFile.readAsBytes()), allowMalformed: true).split("\n");
debugPrint("[ToolsLogAnalyze] logLines: ${logLines.length}");
if (startLine == 0) {
_killCount = 0;
_deathCount = 0;
_selfKillCount = 0;
_vehicleDestructionCount = 0;
_vehicleDestructionCountHard = 0;
_gameStartTime = null;
_gameCrashLineNumber = -1;
} else if (startLine > logLines.length) {
// 考虑文件重新写入的情况
ref.invalidateSelf();
}
_currentLineNumber = logLines.length;
// for i in logLines
for (var i = 0; i < logLines.length; i++) {
// 支持追加模式
if (i < startLine) continue;
final line = logLines[i];
if (line.isEmpty) continue;
final data = _handleLogLine(line, i);
if (data != null) {
_appendResult(data);
// wait for ui update
await Future.delayed(Duration(seconds: 0));
}
}
final lastLineDateTime =
_gameStartTime != null ? _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20"))) : null;
// 检查${S.current.log_analyzer_filter_game_crash}行号
if (_gameCrashLineNumber > 0) {
// crashInfo 从 logLines _gameCrashLineNumber 开始到最后一行
final crashInfo = logLines.sublist(_gameCrashLineNumber);
// 运行一键诊断
final info = SCLoggerHelper.getGameRunningLogInfo(crashInfo);
crashInfo.add(S.current.log_analyzer_one_click_diagnosis_header);
if (info != null) {
crashInfo.add(info.key);
if (info.value.isNotEmpty) {
crashInfo.add(S.current.log_analyzer_details_info(info.value));
}
} else {
crashInfo.add(S.current.log_analyzer_no_crash_detected);
}
_appendResult(LogAnalyzeLineData(
type: "game_crash",
title: S.current.log_analyzer_game_crash,
data: crashInfo.join("\n"),
dateTime: lastLineDateTime != null ? _dateTimeFormatter.format(lastLineDateTime) : null,
));
}
// ${S.current.log_analyzer_kill_summary}
if (_killCount > 0 || _deathCount > 0) {
_appendResult(LogAnalyzeLineData(
type: "statistics",
title: S.current.log_analyzer_kill_summary,
data: S.current.log_analyzer_kill_death_suicide_count(
_killCount,
_deathCount,
_selfKillCount,
_vehicleDestructionCount,
_vehicleDestructionCountHard,
),
));
}
// 统计${S.current.log_analyzer_play_time}_gameStartTime 减去 最后一行的时间
if (_gameStartTime != null) {
if (lastLineDateTime != null) {
final duration = lastLineDateTime.difference(_gameStartTime!);
_appendResult(LogAnalyzeLineData(
type: "statistics",
title: S.current.log_analyzer_play_time,
data: S.current.log_analyzer_play_time_format(
duration.inHours,
duration.inMinutes.remainder(60),
duration.inSeconds.remainder(60),
),
));
}
// 逐条添加结果以支持流式显示
for (final data in results) {
_appendResult(data);
await Future.delayed(Duration.zero); // 让 UI 有机会更新
}
_startListenFile(logFile);
@@ -165,21 +57,21 @@ class ToolsLogAnalyze extends _$ToolsLogAnalyze {
debugPrint("[ToolsLogAnalyze] startListenFile: ${logFile.absolute.path}");
// 监听文件
late final StreamSubscription sub;
sub = FileWatcher(logFile.absolute.path, pollingDelay: Duration(seconds: 1)).events.listen((change) {
sub = FileWatcher(logFile.absolute.path, pollingDelay: const Duration(seconds: 1)).events.listen((change) {
sub.cancel();
if (!_isListenEnabled) return;
_isListenEnabled = false;
debugPrint("[ToolsLogAnalyze] logFile change: ${change.type}");
switch (change.type) {
case ChangeType.MODIFY:
// 移除${S.current.log_analyzer_filter_statistics}
// 移除统计信息
final newList = state.value?.where((e) => e.type != "statistics").toList();
if (listSortReverse) {
state = AsyncData(newList?.reversed.toList() ?? []);
} else {
state = AsyncData(newList ?? []);
}
return _launchLogAnalyze(logFile, startLine: _currentLineNumber);
return _launchLogAnalyze(logFile);
case ChangeType.ADD:
case ChangeType.REMOVE:
ref.invalidateSelf();
@@ -191,67 +83,6 @@ class ToolsLogAnalyze extends _$ToolsLogAnalyze {
});
}
LogAnalyzeLineData? _handleLogLine(String line, int index) {
// 处理 log 行,检测可以提取的内容
if (_gameStartTime == null) {
_gameStartTime = _getLogLineDateTime(line);
return LogAnalyzeLineData(
type: "info",
title: S.current.log_analyzer_game_start,
dateTime: _getLogLineDateTimeString(line),
);
}
// 读取${S.current.log_analyzer_game_loading}时间
final gameLoading = _logGetGameLoading(line);
if (gameLoading != null) {
return LogAnalyzeLineData(
type: "info",
title: S.current.log_analyzer_game_loading,
data: S.current.log_analyzer_mode_loading_time(
gameLoading.$1,
gameLoading.$2,
),
dateTime: _getLogLineDateTimeString(line),
);
}
// 运行基础时间解析器
final baseEvent = _baseEventDecoder(line);
if (baseEvent != null) {
switch (baseEvent) {
case "AccountLoginCharacterStatus_Character":
// 角色登录
return _logGetCharacterName(line);
case "FatalCollision":
// 载具${S.current.log_analyzer_filter_fatal_collision}
return _logGetFatalCollision(line);
case "Vehicle Destruction":
// ${S.current.log_analyzer_filter_vehicle_damaged}
return _logGetVehicleDestruction(line);
case "Actor Death":
// ${S.current.log_analyzer_filter_character_death}
return _logGetActorDeath(line);
case "RequestLocationInventory":
// 请求${S.current.log_analyzer_filter_local_inventory}
return _logGetRequestLocationInventory(line);
}
}
if (line.contains("[CIG] CCIGBroker::FastShutdown")) {
return LogAnalyzeLineData(
type: "info",
title: S.current.log_analyzer_game_close,
dateTime: _getLogLineDateTimeString(line),
);
}
if (line.contains("Cloud Imperium Games public crash handler")) {
_gameCrashLineNumber = index;
}
return null;
}
void _appendResult(LogAnalyzeLineData data) {
// 追加结果到 state
final currentState = state.value;
@@ -266,195 +97,4 @@ class ToolsLogAnalyze extends _$ToolsLogAnalyze {
state = AsyncData([data]);
}
}
final _baseRegExp = RegExp(r'\[Notice\]\s+<([^>]+)>');
String? _baseEventDecoder(String line) {
// 解析 log 行的基本信息
final match = _baseRegExp.firstMatch(line);
if (match != null) {
final type = match.group(1);
return type;
}
return null;
}
final _gameLoadingRegExp =
RegExp(r'<[^>]+>\s+Loading screen for\s+(\w+)\s+:\s+SC_Frontend closed after\s+(\d+\.\d+)\s+seconds');
(String, String)? _logGetGameLoading(String line) {
final match = _gameLoadingRegExp.firstMatch(line);
if (match != null) {
return (match.group(1) ?? "-", match.group(2) ?? "-");
}
return null;
}
final DateFormat _dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss:SSS');
final _logDateTimeRegExp = RegExp(r'<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)>');
DateTime? _getLogLineDateTime(String line) {
// 提取 log 行的时间
final match = _logDateTimeRegExp.firstMatch(line);
if (match != null) {
final dateTimeString = match.group(1);
if (dateTimeString != null) {
return DateTime.parse(dateTimeString).toLocal();
}
}
return null;
}
String? _getLogLineDateTimeString(String line) {
// 提取 log 行的时间
final dateTime = _getLogLineDateTime(line);
if (dateTime != null) {
return _dateTimeFormatter.format(dateTime);
}
return null;
}
// 安全提取函数
String? safeExtract(RegExp pattern, String line) => pattern.firstMatch(line)?.group(1)?.trim();
LogAnalyzeLineData? _logGetFatalCollision(String line) {
final patterns = {
'zone': RegExp(r'\[Part:[^\]]*?Zone:\s*([^,\]]+)'),
'player_pilot': RegExp(r'PlayerPilot:\s*(\d)'),
'hit_entity': RegExp(r'hitting entity:\s*(\w+)'),
'hit_entity_vehicle': RegExp(r'hitting entity:[^\[]*\[Zone:\s*([^\s-]+)'),
'distance': RegExp(r'Distance:\s*([\d.]+)')
};
final zone = safeExtract(patterns['zone']!, line) ?? unknownValue;
final playerPilot = (safeExtract(patterns['player_pilot']!, line) ?? '0') == '1';
final hitEntity = safeExtract(patterns['hit_entity']!, line) ?? unknownValue;
final hitEntityVehicle = safeExtract(patterns['hit_entity_vehicle']!, line) ?? unknownValue;
final distance = double.tryParse(safeExtract(patterns['distance']!, line) ?? '') ?? 0.0;
return LogAnalyzeLineData(
type: "fatal_collision",
title: S.current.log_analyzer_filter_fatal_collision,
data: S.current.log_analyzer_collision_details(
zone,
playerPilot ? '' : '',
hitEntity,
hitEntityVehicle,
distance.toStringAsFixed(2),
),
dateTime: _getLogLineDateTimeString(line),
);
}
LogAnalyzeLineData? _logGetVehicleDestruction(String line) {
final pattern = RegExp(r"Vehicle\s+'([^']+)'.*?" // 载具型号
r"in zone\s+'([^']+)'.*?" // Zone
r"destroy level \d+ to (\d+).*?" // 损毁等级
r"caused by\s+'([^']+)'" // 责任方
);
final match = pattern.firstMatch(line);
if (match != null) {
final vehicleModel = match.group(1) ?? unknownValue;
final zone = match.group(2) ?? unknownValue;
final destructionLevel = int.tryParse(match.group(3) ?? '') ?? 0;
final causedBy = match.group(4) ?? unknownValue;
final destructionLevelMap = {1: S.current.log_analyzer_soft_death, 2: S.current.log_analyzer_disintegration};
if (causedBy.trim() == _playerName) {
if (destructionLevel == 1) {
_vehicleDestructionCount++;
} else if (destructionLevel == 2) {
_vehicleDestructionCountHard++;
}
}
return LogAnalyzeLineData(
type: "vehicle_destruction",
title: S.current.log_analyzer_filter_vehicle_damaged,
data: S.current.log_analyzer_vehicle_damage_details(
vehicleModel,
zone,
destructionLevel.toString(),
destructionLevelMap[destructionLevel] ?? unknownValue,
causedBy,
),
dateTime: _getLogLineDateTimeString(line),
);
}
return null;
}
LogAnalyzeLineData? _logGetActorDeath(String line) {
final pattern = RegExp(r"CActor::Kill: '([^']+)'.*?" // 受害者ID
r"in zone '([^']+)'.*?" // 死亡位置区域
r"killed by '([^']+)'.*?" // 击杀者ID
r"with damage type '([^']+)'" // 伤害类型
);
final match = pattern.firstMatch(line);
if (match != null) {
final victimId = match.group(1) ?? unknownValue;
final zone = match.group(2) ?? unknownValue;
final killerId = match.group(3) ?? unknownValue;
final damageType = match.group(4) ?? unknownValue;
if (victimId.trim() == killerId.trim()) {
// 自杀
_selfKillCount++;
} else {
if (victimId.trim() == _playerName) {
_deathCount++;
}
if (killerId.trim() == _playerName) {
_killCount++;
}
}
return LogAnalyzeLineData(
type: "actor_death",
title: S.current.log_analyzer_filter_character_death,
data: S.current.log_analyzer_death_details(
victimId,
damageType,
killerId,
zone,
),
dateTime: _getLogLineDateTimeString(line),
);
}
return null;
}
LogAnalyzeLineData? _logGetCharacterName(String line) {
final pattern = RegExp(r"name\s+([^-]+)");
final match = pattern.firstMatch(line);
if (match != null) {
final characterName = match.group(1)?.trim() ?? unknownValue;
_playerName = characterName.trim(); // 更新玩家名称
return LogAnalyzeLineData(
type: "player_login",
title: S.current.log_analyzer_player_login(characterName),
dateTime: _getLogLineDateTimeString(line),
);
}
return null;
}
LogAnalyzeLineData? _logGetRequestLocationInventory(String line) {
final pattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
final match = pattern.firstMatch(line);
if (match != null) {
final playerId = match.group(1) ?? unknownValue;
final location = match.group(2) ?? unknownValue;
return LogAnalyzeLineData(
type: "request_location_inventory",
title: S.current.log_analyzer_view_local_inventory,
data: S.current.log_analyzer_player_location(playerId, location),
dateTime: _getLogLineDateTimeString(line),
);
}
return null;
}
}

View File

@@ -1,280 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'log_analyze_provider.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LogAnalyzeLineData {
String get type; String get title; String? get data; String? get dateTime;
/// Create a copy of LogAnalyzeLineData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LogAnalyzeLineDataCopyWith<LogAnalyzeLineData> get copyWith => _$LogAnalyzeLineDataCopyWithImpl<LogAnalyzeLineData>(this as LogAnalyzeLineData, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LogAnalyzeLineData&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.data, data) || other.data == data)&&(identical(other.dateTime, dateTime) || other.dateTime == dateTime));
}
@override
int get hashCode => Object.hash(runtimeType,type,title,data,dateTime);
@override
String toString() {
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
}
}
/// @nodoc
abstract mixin class $LogAnalyzeLineDataCopyWith<$Res> {
factory $LogAnalyzeLineDataCopyWith(LogAnalyzeLineData value, $Res Function(LogAnalyzeLineData) _then) = _$LogAnalyzeLineDataCopyWithImpl;
@useResult
$Res call({
String type, String title, String? data, String? dateTime
});
}
/// @nodoc
class _$LogAnalyzeLineDataCopyWithImpl<$Res>
implements $LogAnalyzeLineDataCopyWith<$Res> {
_$LogAnalyzeLineDataCopyWithImpl(this._self, this._then);
final LogAnalyzeLineData _self;
final $Res Function(LogAnalyzeLineData) _then;
/// Create a copy of LogAnalyzeLineData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? title = null,Object? data = freezed,Object? dateTime = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as String?,dateTime: freezed == dateTime ? _self.dateTime : dateTime // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [LogAnalyzeLineData].
extension LogAnalyzeLineDataPatterns on LogAnalyzeLineData {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _LogAnalyzeLineData value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _LogAnalyzeLineData() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _LogAnalyzeLineData value) $default,){
final _that = this;
switch (_that) {
case _LogAnalyzeLineData():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _LogAnalyzeLineData value)? $default,){
final _that = this;
switch (_that) {
case _LogAnalyzeLineData() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String title, String? data, String? dateTime)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LogAnalyzeLineData() when $default != null:
return $default(_that.type,_that.title,_that.data,_that.dateTime);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String title, String? data, String? dateTime) $default,) {final _that = this;
switch (_that) {
case _LogAnalyzeLineData():
return $default(_that.type,_that.title,_that.data,_that.dateTime);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String title, String? data, String? dateTime)? $default,) {final _that = this;
switch (_that) {
case _LogAnalyzeLineData() when $default != null:
return $default(_that.type,_that.title,_that.data,_that.dateTime);case _:
return null;
}
}
}
/// @nodoc
class _LogAnalyzeLineData implements LogAnalyzeLineData {
const _LogAnalyzeLineData({required this.type, required this.title, this.data, this.dateTime});
@override final String type;
@override final String title;
@override final String? data;
@override final String? dateTime;
/// Create a copy of LogAnalyzeLineData
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LogAnalyzeLineDataCopyWith<_LogAnalyzeLineData> get copyWith => __$LogAnalyzeLineDataCopyWithImpl<_LogAnalyzeLineData>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LogAnalyzeLineData&&(identical(other.type, type) || other.type == type)&&(identical(other.title, title) || other.title == title)&&(identical(other.data, data) || other.data == data)&&(identical(other.dateTime, dateTime) || other.dateTime == dateTime));
}
@override
int get hashCode => Object.hash(runtimeType,type,title,data,dateTime);
@override
String toString() {
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
}
}
/// @nodoc
abstract mixin class _$LogAnalyzeLineDataCopyWith<$Res> implements $LogAnalyzeLineDataCopyWith<$Res> {
factory _$LogAnalyzeLineDataCopyWith(_LogAnalyzeLineData value, $Res Function(_LogAnalyzeLineData) _then) = __$LogAnalyzeLineDataCopyWithImpl;
@override @useResult
$Res call({
String type, String title, String? data, String? dateTime
});
}
/// @nodoc
class __$LogAnalyzeLineDataCopyWithImpl<$Res>
implements _$LogAnalyzeLineDataCopyWith<$Res> {
__$LogAnalyzeLineDataCopyWithImpl(this._self, this._then);
final _LogAnalyzeLineData _self;
final $Res Function(_LogAnalyzeLineData) _then;
/// Create a copy of LogAnalyzeLineData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? title = null,Object? data = freezed,Object? dateTime = freezed,}) {
return _then(_LogAnalyzeLineData(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as String?,dateTime: freezed == dateTime ? _self.dateTime : dateTime // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on

View File

@@ -50,7 +50,7 @@ final class ToolsLogAnalyzeProvider
}
}
String _$toolsLogAnalyzeHash() => r'5666c3f882e22e2192593629164bc53f8ce4aabe';
String _$toolsLogAnalyzeHash() => r'f5079c7d35daf25b07f83bacb224484171e9c93f';
final class ToolsLogAnalyzeFamily extends $Family
with