feat: PartyRoomGameLogTrackerProvider

This commit is contained in:
xkeyC 2025-11-20 23:58:31 +08:00
parent f551ccfbde
commit 3e9f82ecdf
22 changed files with 320 additions and 140 deletions

View File

@ -15,8 +15,8 @@ class LogAnalyzeLineData {
//
final String? victimId; // ID (actor_death)
final String? killerId; // ID (actor_death)
final String? location; // (request_location_inventory)
final String? area; //
final String? playerName; // (player_login)
const LogAnalyzeLineData({
@ -26,8 +26,8 @@ class LogAnalyzeLineData {
this.dateTime,
this.tag,
this.victimId,
this.killerId,
this.location,
this.area,
this.playerName,
});
@ -76,10 +76,10 @@ class GameLogAnalyzer {
//
static final _fatalCollisionPatterns = {
'zone': RegExp(r'\[Part:[^\]]*?Zone:\s*([^,\]]+)'),
'vehicle': RegExp(r'Fatal Collision occured for vehicle\s+(\S+)'),
'zone': RegExp(r'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.]+)'),
};
@ -93,10 +93,9 @@ class GameLogAnalyzer {
//
static final _actorDeathPattern = RegExp(
r"CActor::Kill: '([^']+)'.*?" // ID
r"in zone '([^']+)'.*?" //
r"killed by '([^']+)'.*?" // ID
r"with damage type '([^']+)'", //
r"Actor '([^']+)'.*?" // ID
r"ejected from zone '([^']+)'.*?" // /
r"to zone '([^']+)'", //
);
//
@ -217,7 +216,7 @@ class GameLogAnalyzer {
}
});
break;
case "Actor Death":
case "[ActorState] Dead":
data = _parseActorDeath(line, playerName, shouldCount, (isKill, isDeath, isSelfKill) {
if (isSelfKill) {
selfKillCount++;
@ -372,10 +371,10 @@ class GameLogAnalyzer {
static String? _safeExtract(RegExp pattern, String line) => pattern.firstMatch(line)?.group(1)?.trim();
static LogAnalyzeLineData? _parseFatalCollision(String line) {
final vehicle = _safeExtract(_fatalCollisionPatterns['vehicle']!, line) ?? unknownValue;
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(
@ -385,7 +384,7 @@ class GameLogAnalyzer {
zone,
playerPilot ? '' : '',
hitEntity,
hitEntityVehicle,
vehicle,
distance.toStringAsFixed(2),
),
dateTime: _getLogLineDateTimeString(line),
@ -436,27 +435,25 @@ class GameLogAnalyzer {
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;
final fromZone = match.group(2) ?? unknownValue;
final toZone = match.group(3) ?? 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);
final isDeath = victimId.trim() == playerName;
if (isDeath) {
onDeath(false, true, 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),
data: S.current.log_analyzer_death_details(victimId, fromZone, toZone),
dateTime: _getLogLineDateTimeString(line),
victimId: victimId, //
killerId: killerId, //
location: fromZone,
area: toZone,
victimId: victimId,
playerName: playerName,
);
}
return null;

View File

@ -130,8 +130,8 @@ class MessageLookup extends MessageLookupByLibrary {
static String m46(v0, v1, v2, v3, v4) =>
"Area: ${v0} Player driving: ${v1} Collision entity: ${v2} \nCollision vehicle: ${v3} Collision distance: ${v4} ";
static String m47(v0, v1, v2, v3) =>
"Victim ID: ${v0} Cause of death: ${v1} \nKiller ID: ${v2} \nArea: ${v3}";
static String m47(v0, v2, v3) =>
"Victim ID: ${v0} \nLocation: ${v2} \nArea: ${v3}";
static String m48(v0) => "Detailed information: ${v0}";

View File

@ -119,8 +119,7 @@ class MessageLookup extends MessageLookupByLibrary {
static String m46(v0, v1, v2, v3, v4) =>
"エリア:${v0} プレイヤー操縦:${v1} 衝突エンティティ:${v2} \n衝突ビークル:${v3} 衝突距離:${v4} ";
static String m47(v0, v1, v2, v3) =>
"被害者ID${v0} 死因:${v1} \n殺害者ID${v2} \nエリア:${v3}";
static String m47(v0, v2, v3) => "被害者ID${v0} \n位置:${v2} \nエリア:${v3}";
static String m48(v0) => "詳細情報:${v0}";

View File

@ -124,8 +124,8 @@ class MessageLookup extends MessageLookupByLibrary {
static String m46(v0, v1, v2, v3, v4) =>
"Зона: ${v0} Управление игроком: ${v1} Объект столкновения: ${v2} \nТехника столкновения: ${v3} Дистанция столкновения: ${v4} ";
static String m47(v0, v1, v2, v3) =>
"ID жертвы: ${v0} Причина смерти: ${v1} \nID убийцы: ${v2} \nЗона: ${v3}";
static String m47(v0, v2, v3) =>
"ID жертвы: ${v0} \nID убийцы: ${v2} \nЗона: ${v3}";
static String m48(v0) => "Подробная информация: ${v0}";

View File

@ -119,8 +119,7 @@ class MessageLookup extends MessageLookupByLibrary {
static String m46(v0, v1, v2, v3, v4) =>
"区域:${v0} 玩家驾驶:${v1} 碰撞实体:${v2} \n碰撞载具: ${v3} 碰撞距离:${v4} ";
static String m47(v0, v1, v2, v3) =>
"受害者ID${v0} 死因:${v1} \n击杀者ID${v2} \n区域:${v3}";
static String m47(v0, v2, v3) => "受害者ID${v0} \n位置:${v2} \n区域:${v3}";
static String m48(v0) => "详细信息:${v0}";
@ -1406,7 +1405,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tools_rsi_launcher_enhance_init_msg2":
MessageLookupByLibrary.simpleMessage("正在从网络获取增强数据..."),
"tools_rsi_launcher_enhance_msg_error":
MessageLookupByLibrary.simpleMessage("获取增强数据失败,可能是网络问题或当前版本不支持"),
MessageLookupByLibrary.simpleMessage("当前版本不支持,請等待適配..."),
"tools_rsi_launcher_enhance_msg_error_get_launcher_info_error":
MessageLookupByLibrary.simpleMessage("读取启动器信息失败!"),
"tools_rsi_launcher_enhance_msg_error_get_launcher_info_error_with_args":

View File

@ -115,8 +115,7 @@ class MessageLookup extends MessageLookupByLibrary {
static String m46(v0, v1, v2, v3, v4) =>
"區域:${v0} 玩家駕駛:${v1} 碰撞實體:${v2} \n碰撞載具: ${v3} 碰撞距離:${v4} ";
static String m47(v0, v1, v2, v3) =>
"受害者ID${v0} 死因:${v1} \n擊殺者ID${v2} \n區域:${v3}";
static String m47(v0, v2, v3) => "受害者ID${v0} \n位置:${v2} \n區域:${v3}";
static String m48(v0) => "詳細資訊:${v0}";
@ -1397,7 +1396,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tools_rsi_launcher_enhance_init_msg2":
MessageLookupByLibrary.simpleMessage("正在從網路取得增強資料..."),
"tools_rsi_launcher_enhance_msg_error":
MessageLookupByLibrary.simpleMessage("增強資料取得失敗,可能是網路問題或目前版本不支援"),
MessageLookupByLibrary.simpleMessage("目前版本不支援"),
"tools_rsi_launcher_enhance_msg_error_get_launcher_info_error":
MessageLookupByLibrary.simpleMessage("讀取啟動器資訊失敗!"),
"tools_rsi_launcher_enhance_msg_error_get_launcher_info_error_with_args":

View File

@ -5808,18 +5808,13 @@ class S {
);
}
/// `Victim ID: {v0} Cause of death: {v1} \nKiller ID: {v2} \nArea: {v3}`
String log_analyzer_death_details(
Object v0,
Object v1,
Object v2,
Object v3,
) {
/// `Victim ID: {v0} \nLocation: {v2} \nArea: {v3}`
String log_analyzer_death_details(Object v0, Object v2, Object v3) {
return Intl.message(
'Victim ID: $v0 Cause of death: $v1 \nKiller ID: $v2 \nArea: $v3',
'Victim ID: $v0 \nLocation: $v2 \nArea: $v3',
name: 'log_analyzer_death_details',
desc: '',
args: [v0, v1, v2, v3],
args: [v0, v2, v3],
);
}

View File

@ -1141,7 +1141,7 @@
"@log_analyzer_disintegration": {},
"log_analyzer_vehicle_damage_details": "Vehicle model: {v0} \nArea: {v1} \nDamage level: {v2} ({v3}) Responsible party: {v4}",
"@log_analyzer_vehicle_damage_details": {},
"log_analyzer_death_details": "Victim ID: {v0} Cause of death: {v1} \nKiller ID: {v2} \nArea: {v3}",
"log_analyzer_death_details": "Victim ID: {v0} \nLocation: {v2} \nArea: {v3}",
"@log_analyzer_death_details": {},
"log_analyzer_player_login": "Player {v0} logged in...",
"@log_analyzer_player_login": {},

View File

@ -1139,7 +1139,7 @@
"@log_analyzer_disintegration": {},
"log_analyzer_vehicle_damage_details": "ビークルモデル:{v0} \nエリア{v1} \n損傷レベル{v2} {v3} 責任者:{v4}",
"@log_analyzer_vehicle_damage_details": {},
"log_analyzer_death_details": "被害者ID{v0} 死因:{v1} \n殺害者ID{v2} \nエリア{v3}",
"log_analyzer_death_details": "被害者ID{v0} \n位置{v2} \nエリア{v3}",
"@log_analyzer_death_details": {},
"log_analyzer_player_login": "プレイヤー {v0} ログイン中...",
"@log_analyzer_player_login": {},

View File

@ -1139,7 +1139,7 @@
"@log_analyzer_disintegration": {},
"log_analyzer_vehicle_damage_details": "Модель техники: {v0} \nЗона: {v1} \nУровень повреждения: {v2} ({v3}) Виновник: {v4}",
"@log_analyzer_vehicle_damage_details": {},
"log_analyzer_death_details": "ID жертвы: {v0} Причина смерти: {v1} \nID убийцы: {v2} \nЗона: {v3}",
"log_analyzer_death_details": "ID жертвы: {v0} \nID убийцы: {v2} \nЗона: {v3}",
"@log_analyzer_death_details": {},
"log_analyzer_player_login": "Игрок {v0} входит в игру...",
"@log_analyzer_player_login": {},

View File

@ -772,7 +772,7 @@
"tools_rsi_launcher_enhance_title": "RSI 启动器增强",
"tools_rsi_launcher_enhance_msg_version": "启动器内部版本信息:{v0}",
"tools_rsi_launcher_enhance_msg_patch_status": "补丁状态:{v0}",
"tools_rsi_launcher_enhance_msg_error": "获取增强数据失败,可能是网络问题或当前版本不支持",
"tools_rsi_launcher_enhance_msg_error": "当前版本不支持,請等待適配...",
"tools_rsi_launcher_enhance_title_localization": "RSI 启动器本地化",
"tools_rsi_launcher_enhance_subtitle_localization": "为 RSI 启动器增加多语言支持。",
"tools_rsi_launcher_enhance_title_download_booster": "RSI 启动器下载增强",
@ -890,7 +890,7 @@
"log_analyzer_soft_death": "软死亡",
"log_analyzer_disintegration": "解体",
"log_analyzer_vehicle_damage_details": "载具型号:{v0} \n区域{v1} \n损毁等级{v2} {v3} 责任方:{v4}",
"log_analyzer_death_details": "受害者ID{v0} 死因:{v1} \n击杀者ID{v2} \n区域{v3}",
"log_analyzer_death_details": "受害者ID{v0} \n位置{v2} \n区域{v3}",
"log_analyzer_player_login": "玩家 {v0} 登录 ...",
"log_analyzer_view_local_inventory": "查看本地库存",
"log_analyzer_player_location": "玩家ID{v0} 位置:{v1}",

View File

@ -907,7 +907,7 @@
"@tools_rsi_launcher_enhance_msg_version": {},
"tools_rsi_launcher_enhance_msg_patch_status": "補丁狀態:{v0}",
"@tools_rsi_launcher_enhance_msg_patch_status": {},
"tools_rsi_launcher_enhance_msg_error": "增強資料取得失敗,可能是網路問題或目前版本不支援",
"tools_rsi_launcher_enhance_msg_error": "目前版本不支援",
"@tools_rsi_launcher_enhance_msg_error": {},
"tools_rsi_launcher_enhance_title_localization": "RSI 啟動器本地化",
"@tools_rsi_launcher_enhance_title_localization": {},
@ -1141,7 +1141,7 @@
"@log_analyzer_disintegration": {},
"log_analyzer_vehicle_damage_details": "載具型號:{v0} \n區域{v1} \n損毀等級{v2} {v3} 責任方:{v4}",
"@log_analyzer_vehicle_damage_details": {},
"log_analyzer_death_details": "受害者ID{v0} 死因:{v1} \n擊殺者ID{v2} \n區域{v3}",
"log_analyzer_death_details": "受害者ID{v0} \n位置{v2} \n區域{v3}",
"@log_analyzer_death_details": {},
"log_analyzer_player_login": "玩家 {v0} 登入 ...",
"@log_analyzer_player_login": {},

View File

@ -75,7 +75,11 @@ class PartyRoom extends _$PartyRoom {
Box? _confBox;
StreamSubscription<partroom.RoomEvent>? _eventStreamSubscription;
Timer? _heartbeatTimer;
Timer? _reconnectTimer;
bool _disposed = false;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
static const Duration _reconnectDelay = Duration(seconds: 3);
@override
PartyRoomFullState build() {
@ -398,6 +402,7 @@ class PartyRoom extends _$PartyRoom {
await _stopHeartbeat();
await _stopEventStream();
_reconnectAttempts = 0;
_dismissRoom();
dPrint('[PartyRoom] Left room: $roomUuid');
@ -421,6 +426,7 @@ class PartyRoom extends _$PartyRoom {
await _stopHeartbeat();
await _stopEventStream();
_reconnectAttempts = 0;
_dismissRoom();
dPrint('[PartyRoom] Dismissed room: $roomUuid');
@ -754,24 +760,106 @@ class PartyRoom extends _$PartyRoom {
_eventStreamSubscription = stream.listen(
(event) {
//
_reconnectAttempts = 0;
_handleRoomEvent(event);
},
onError: (error) {
dPrint('[PartyRoom] Event stream error: $error');
//
_scheduleReconnect(roomUuid);
},
onDone: () {
dPrint('[PartyRoom] Event stream closed');
//
_scheduleReconnect(roomUuid);
},
);
dPrint('[PartyRoom] Event stream started');
//
_reconnectAttempts = 0;
} catch (e) {
dPrint('[PartyRoom] StartEventStream error: $e');
//
_scheduleReconnect(roomUuid);
}
}
///
void _scheduleReconnect(String roomUuid) {
//
if (_disposed || state.room.roomUuid == null || state.room.roomUuid != roomUuid) {
dPrint('[PartyRoom] Skip reconnect: disposed=$_disposed, roomUuid=${state.room.roomUuid}');
return;
}
//
if (_reconnectTimer?.isActive ?? false) {
return;
}
//
if (_reconnectAttempts >= _maxReconnectAttempts) {
dPrint('[PartyRoom] Max reconnect attempts reached ($_maxReconnectAttempts)');
return;
}
_reconnectAttempts++;
dPrint(
'[PartyRoom] Scheduling reconnect attempt $_reconnectAttempts/$_maxReconnectAttempts in ${_reconnectDelay.inSeconds}s',
);
_reconnectTimer = Timer(_reconnectDelay, () async {
await _attemptReconnect(roomUuid);
});
}
///
Future<void> _attemptReconnect(String roomUuid) async {
if (_disposed || state.room.roomUuid == null || state.room.roomUuid != roomUuid) {
dPrint('[PartyRoom] Abort reconnect: no longer in room');
return;
}
try {
dPrint('[PartyRoom] Attempting to reconnect event stream...');
//
final client = state.client.roomClient;
if (client == null) {
dPrint('[PartyRoom] Reconnect failed: client not available');
_scheduleReconnect(roomUuid);
return;
}
// getMyRoom
final response = await client.getMyRoom(partroom.GetMyRoomRequest(), options: _getAuthCallOptions());
if (!response.hasRoom() || response.room.roomUuid != roomUuid) {
dPrint('[PartyRoom] Reconnect failed: no longer in room');
//
await _stopHeartbeat();
await _stopEventStream();
_reconnectAttempts = 0;
_dismissRoom();
return;
}
//
dPrint('[PartyRoom] Still in room, restarting event stream');
await _startEventStream(roomUuid);
} catch (e) {
dPrint('[PartyRoom] Reconnect attempt failed: $e');
//
_scheduleReconnect(roomUuid);
}
}
///
Future<void> _stopEventStream() async {
_reconnectTimer?.cancel();
_reconnectTimer = null;
await _eventStreamSubscription?.cancel();
_eventStreamSubscription = null;
dPrint('[PartyRoom] Event stream stopped');
@ -794,6 +882,21 @@ class PartyRoom extends _$PartyRoom {
case partroom.RoomEventType.MEMBER_JOINED:
case partroom.RoomEventType.MEMBER_LEFT:
case partroom.RoomEventType.MEMBER_KICKED:
if (event.type == partroom.RoomEventType.MEMBER_KICKED) {
//
if (event.member.gameUserId == state.auth.userInfo?.gameUserId) {
//
_dismissRoom();
return;
}
}
//
if (event.type == partroom.RoomEventType.MEMBER_LEFT) {
if (event.member.gameUserId == state.auth.userInfo?.gameUserId) {
//
return;
}
}
//
if (state.room.roomUuid != null) {
getRoomMembers(state.room.roomUuid!);

View File

@ -44,7 +44,7 @@ final class PartyRoomProvider
}
}
String _$partyRoomHash() => r'02cdd156995799411eb47107d5c197f43e78629e';
String _$partyRoomHash() => r'd7182854c8caf5bb362c45a4e6e2ab40c2ef1b09';
/// PartyRoom Provider

View File

@ -105,6 +105,12 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
playTime: currentGameStartTime != gameStartTime ? gameStartTime : null,
);
}
if (next.deathEvents?.isNotEmpty ?? false) {
for (final event in next.deathEvents!) {
ref.read(partyRoomProvider.notifier).sendSignal("special_death", params: {"location": event.$1, "area": event.$2});
}
}
}
///

View File

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

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -18,8 +19,7 @@ sealed class PartyRoomGameLogTrackerProviderState with _$PartyRoomGameLogTracker
@Default(0) int kills,
@Default(0) int deaths,
DateTime? gameStartTime,
@Default([]) List<String> killedIds, // ID
@Default([]) List<String> deathIds, // ID
List<(String, String)>? deathEvents,
}) = _PartyRoomGameLogTrackerProviderState;
}
@ -32,6 +32,7 @@ class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider
@override
PartyRoomGameLogTrackerProviderState build({required DateTime startTime}) {
startTime = DateTime.now();
dPrint("[PartyRoomGameLogTrackerProvider] init $startTime");
ref.onDispose(() {
_disposed = true;
@ -65,16 +66,9 @@ class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider
}
} catch (e) {
//
state = state.copyWith(
location: '<游戏未启动>',
gameStartTime: null,
kills: 0,
deaths: 0,
killedIds: [],
deathIds: [],
);
state = state.copyWith(location: '<游戏未启动>', gameStartTime: null, kills: 0, deaths: 0);
}
await Future.delayed(const Duration(seconds: 5));
await Future.delayed(const Duration(seconds: 10));
}
}
@ -90,9 +84,7 @@ class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider
//
final location = statistics.latestLocation == null ? '<主菜单>' : '[${statistics.latestLocation}]';
// _lastQueryTime ID
final newKilledIds = <String>[];
final newDeathIds = <String>[];
List<(String, String)> deathEvents = [];
if (_lastQueryTime != null) {
// actor_death
@ -125,24 +117,15 @@ class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider
}
}
// 使
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);
}
if (data.playerName == data.victimId) {
//
deathEvents.add((data.location ?? "-", data.area ?? "-"));
}
}
}
// debugPrint(
// "[PartyRoomGameLogTrackerProvider] location $location killCount ${statistics.killCount} deathCount ${statistics.deathCount}",
// );
//
state = state.copyWith(
location: location,
@ -151,10 +134,7 @@ class PartyRoomGameLogTrackerProvider extends _$PartyRoomGameLogTrackerProvider
deaths: statistics.deathCount,
// startTime
gameStartTime: statistics.gameStartTime,
//
killedIds: newKilledIds,
//
deathIds: newDeathIds, //
deathEvents: deathEvents,
);
//

View File

@ -12,10 +12,9 @@ part of 'game_log_tracker_provider.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PartyRoomGameLogTrackerProviderState {
mixin _$PartyRoomGameLogTrackerProviderState implements DiagnosticableTreeMixin {
String get location; int get kills; int get deaths; DateTime? get gameStartTime; List<String> get killedIds;// ID
List<String> get deathIds;
String get location; int get kills; int get deaths; DateTime? get gameStartTime; List<(String, String)>? get deathEvents;
/// Create a copy of PartyRoomGameLogTrackerProviderState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -23,19 +22,25 @@ mixin _$PartyRoomGameLogTrackerProviderState {
$PartyRoomGameLogTrackerProviderStateCopyWith<PartyRoomGameLogTrackerProviderState> get copyWith => _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<PartyRoomGameLogTrackerProviderState>(this as PartyRoomGameLogTrackerProviderState, _$identity);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'PartyRoomGameLogTrackerProviderState'))
..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('kills', kills))..add(DiagnosticsProperty('deaths', deaths))..add(DiagnosticsProperty('gameStartTime', gameStartTime))..add(DiagnosticsProperty('deathEvents', deathEvents));
}
@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));
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.deathEvents, deathEvents));
}
@override
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(killedIds),const DeepCollectionEquality().hash(deathIds));
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(deathEvents));
@override
String toString() {
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, deathEvents: $deathEvents)';
}
@ -46,7 +51,7 @@ 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
String location, int kills, int deaths, DateTime? gameStartTime, List<(String, String)>? deathEvents
});
@ -63,15 +68,14 @@ class _$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
/// 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,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? deathEvents = freezed,}) {
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>,
as DateTime?,deathEvents: freezed == deathEvents ? _self.deathEvents : deathEvents // ignore: cast_nullable_to_non_nullable
as List<(String, String)>?,
));
}
@ -153,10 +157,10 @@ return $default(_that);case _:
/// }
/// ```
@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;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<(String, String)>? deathEvents)? $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 $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.deathEvents);case _:
return orElse();
}
@ -174,10 +178,10 @@ return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_tha
/// }
/// ```
@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;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String location, int kills, int deaths, DateTime? gameStartTime, List<(String, String)>? deathEvents) $default,) {final _that = this;
switch (_that) {
case _PartyRoomGameLogTrackerProviderState():
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.killedIds,_that.deathIds);}
return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.deathEvents);}
}
/// A variant of `when` that fallback to returning `null`
///
@ -191,10 +195,10 @@ return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_tha
/// }
/// ```
@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;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String location, int kills, int deaths, DateTime? gameStartTime, List<(String, String)>? deathEvents)? $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 $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_that.deathEvents);case _:
return null;
}
@ -205,28 +209,21 @@ return $default(_that.location,_that.kills,_that.deaths,_that.gameStartTime,_tha
/// @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;
class _PartyRoomGameLogTrackerProviderState with DiagnosticableTreeMixin implements PartyRoomGameLogTrackerProviderState {
const _PartyRoomGameLogTrackerProviderState({this.location = '', this.kills = 0, this.deaths = 0, this.gameStartTime, final List<(String, String)>? deathEvents}): _deathEvents = deathEvents;
@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;
final List<(String, String)>? _deathEvents;
@override List<(String, String)>? get deathEvents {
final value = _deathEvents;
if (value == null) return null;
if (_deathEvents is EqualUnmodifiableListView) return _deathEvents;
// 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);
return EqualUnmodifiableListView(value);
}
@ -237,19 +234,25 @@ class _PartyRoomGameLogTrackerProviderState implements PartyRoomGameLogTrackerPr
_$PartyRoomGameLogTrackerProviderStateCopyWith<_PartyRoomGameLogTrackerProviderState> get copyWith => __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<_PartyRoomGameLogTrackerProviderState>(this, _$identity);
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'PartyRoomGameLogTrackerProviderState'))
..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('kills', kills))..add(DiagnosticsProperty('deaths', deaths))..add(DiagnosticsProperty('gameStartTime', gameStartTime))..add(DiagnosticsProperty('deathEvents', deathEvents));
}
@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));
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._deathEvents, _deathEvents));
}
@override
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(_killedIds),const DeepCollectionEquality().hash(_deathIds));
int get hashCode => Object.hash(runtimeType,location,kills,deaths,gameStartTime,const DeepCollectionEquality().hash(_deathEvents));
@override
String toString() {
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, killedIds: $killedIds, deathIds: $deathIds)';
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'PartyRoomGameLogTrackerProviderState(location: $location, kills: $kills, deaths: $deaths, gameStartTime: $gameStartTime, deathEvents: $deathEvents)';
}
@ -260,7 +263,7 @@ abstract mixin class _$PartyRoomGameLogTrackerProviderStateCopyWith<$Res> implem
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
String location, int kills, int deaths, DateTime? gameStartTime, List<(String, String)>? deathEvents
});
@ -277,15 +280,14 @@ class __$PartyRoomGameLogTrackerProviderStateCopyWithImpl<$Res>
/// 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,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? location = null,Object? kills = null,Object? deaths = null,Object? gameStartTime = freezed,Object? deathEvents = freezed,}) {
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>,
as DateTime?,deathEvents: freezed == deathEvents ? _self._deathEvents : deathEvents // ignore: cast_nullable_to_non_nullable
as List<(String, String)>?,
));
}

View File

@ -66,7 +66,7 @@ final class PartyRoomGameLogTrackerProviderProvider
}
String _$partyRoomGameLogTrackerProviderHash() =>
r'ecb015eb46d25bfe11bbb153242fd5c4f20ef367';
r'3e1560b2fffc5461a41bece57b43e27f4112ad0c';
final class PartyRoomGameLogTrackerProviderFamily extends $Family
with

View File

@ -97,15 +97,12 @@ class PartyRoomMemberItem extends ConsumerWidget {
),
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)),
Expanded(
child: Text(
member.status.currentLocation.isNotEmpty ? member.status.currentLocation : '...',
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .9)),
overflow: TextOverflow.ellipsis,
),
),
],
),

View File

@ -210,6 +210,11 @@ class _MessageItem extends ConsumerWidget {
final userName = _getEventUserName(roomEvent);
final avatarUrl = _getEventAvatarUrl(roomEvent);
//
if (isSignal && roomEvent.signalId == 'special_death') {
return _buildDeathMessageCard(context, roomEvent, userName, avatarUrl);
}
final text = _getEventText(roomEvent, ref);
if (text == null) return const SizedBox.shrink();
@ -259,6 +264,104 @@ class _MessageItem extends ConsumerWidget {
);
}
Widget _buildDeathMessageCard(
BuildContext context,
partroom.RoomEvent roomEvent,
String userName,
String? avatarUrl,
) {
final location = roomEvent.signalParams['location'] ?? '未知位置';
final area = roomEvent.signalParams['area'] ?? '未知区域';
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserAvatar(userName, avatarUrl: avatarUrl, size: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Row(
children: [
Text(
userName,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white),
),
const SizedBox(width: 8),
Text(
_formatTime(roomEvent.timestamp),
style: const TextStyle(fontSize: 11, color: Color(0xFF80848E)),
),
],
),
const SizedBox(height: 8),
//
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF2B2D31),
border: Border.all(color: const Color(0xFFED4245).withValues(alpha: 0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: const Color(0xFFED4245).withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(FluentIcons.status_error_full, size: 14, color: Color(0xFFED4245)),
),
const SizedBox(width: 8),
const Text(
'玩家死亡',
style: TextStyle(fontSize: 14, color: Color(0xFFED4245), fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 12),
//
_buildInfoRow(icon: FluentIcons.location, label: '位置', value: location),
const SizedBox(height: 8),
//
_buildInfoRow(icon: FluentIcons.map_pin, label: '区域', value: area),
],
),
),
],
),
),
],
),
);
}
Widget _buildInfoRow({required IconData icon, required String label, required String value}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 14, color: Colors.white.withValues(alpha: .4)),
const SizedBox(width: 8),
Text(
'$label: ',
style: TextStyle(fontSize: 13, color: Colors.white.withValues(alpha: .4), fontWeight: FontWeight.w500),
),
Expanded(
child: Text(value, style: const TextStyle(fontSize: 13, color: Color(0xFFDBDEE1))),
),
],
);
}
String _getEventUserName(partroom.RoomEvent event) {
switch (event.type) {
case partroom.RoomEventType.SIGNAL_BROADCAST:

View File

@ -123,7 +123,7 @@ class PartyRoomRegisterPage extends HookConsumerWidget {
? const Color(0xFF4CAF50)
: isActive
? const Color(0xFF4A9EFF)
: Colors.grey.withValues(alpha: 0.3),
: Colors.white.withValues(alpha: 0.4),
shape: BoxShape.circle,
),
child: Center(
@ -143,7 +143,7 @@ class PartyRoomRegisterPage extends HookConsumerWidget {
title,
style: TextStyle(
fontSize: 11,
color: isActive ? const Color(0xFF4A9EFF) : Colors.grey.withValues(alpha: 0.7),
color: isActive ? const Color(0xFF4A9EFF) : Colors.white.withValues(alpha: 0.4),
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),