mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-13 19:50:28 +00:00
493 lines
16 KiB
Dart
493 lines
16 KiB
Dart
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:intl/intl.dart';
|
||
import 'package:starcitizen_doctor/common/helper/log_helper.dart';
|
||
import 'package:starcitizen_doctor/generated/l10n.dart';
|
||
|
||
/// 日志分析结果数据类
|
||
class LogAnalyzeLineData {
|
||
final String type;
|
||
final String title;
|
||
final String? data;
|
||
final String? dateTime;
|
||
final String? tag; // 统计标签,用于定位日志(如 "game_start"),不依赖本地化
|
||
|
||
// 格式化后的字段
|
||
final String? victimId; // 受害者ID (actor_death)
|
||
final String? location; // 位置信息 (request_location_inventory)
|
||
final String? area; // 区域信息
|
||
final String? playerName; // 玩家名称 (player_login)
|
||
|
||
const LogAnalyzeLineData({
|
||
required this.type,
|
||
required this.title,
|
||
this.data,
|
||
this.dateTime,
|
||
this.tag,
|
||
this.victimId,
|
||
this.location,
|
||
this.area,
|
||
this.playerName,
|
||
});
|
||
|
||
@override
|
||
String toString() {
|
||
return 'LogAnalyzeLineData(type: $type, title: $title, data: $data, dateTime: $dateTime)';
|
||
}
|
||
}
|
||
|
||
/// 日志分析统计数据
|
||
class LogAnalyzeStatistics {
|
||
final String playerName;
|
||
final int killCount;
|
||
final int deathCount;
|
||
final int selfKillCount;
|
||
final int vehicleDestructionCount;
|
||
final int vehicleDestructionCountHard;
|
||
final DateTime? gameStartTime;
|
||
final int gameCrashLineNumber;
|
||
final String? latestLocation; // 最新位置信息(全量查找)
|
||
|
||
const LogAnalyzeStatistics({
|
||
required this.playerName,
|
||
required this.killCount,
|
||
required this.deathCount,
|
||
required this.selfKillCount,
|
||
required this.vehicleDestructionCount,
|
||
required this.vehicleDestructionCountHard,
|
||
this.gameStartTime,
|
||
required this.gameCrashLineNumber,
|
||
this.latestLocation,
|
||
});
|
||
}
|
||
|
||
/// 游戏日志分析器
|
||
class GameLogAnalyzer {
|
||
static const String unknownValue = "<Unknown>";
|
||
|
||
// 正则表达式定义
|
||
static final _baseRegExp = RegExp(r'\[Notice\]\s+<([^>]+)>');
|
||
static final _gameLoadingRegExp = RegExp(
|
||
r'<[^>]+>\s+Loading screen for\s+(\w+)\s+:\s+SC_Frontend closed after\s+(\d+\.\d+)\s+seconds',
|
||
);
|
||
static final _logDateTimeRegExp = RegExp(r'<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)>');
|
||
static final DateFormat _dateTimeFormatter = DateFormat('yyyy-MM-dd HH:mm:ss:SSS');
|
||
|
||
// 致命碰撞解析
|
||
static final _fatalCollisionPatterns = {
|
||
'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+)'),
|
||
'distance': RegExp(r'Distance:\s*([\d.]+)'),
|
||
};
|
||
|
||
// 载具损毁解析
|
||
static final _vehicleDestructionPattern = RegExp(
|
||
r"Vehicle\s+'([^']+)'.*?" // 载具型号
|
||
r"in zone\s+'([^']+)'.*?" // Zone
|
||
r"destroy level \d+ to (\d+).*?" // 损毁等级
|
||
r"caused by\s+'([^']+)'", // 责任方
|
||
);
|
||
|
||
// 角色死亡解析
|
||
static final _actorDeathPattern = RegExp(
|
||
r"Actor '([^']+)'.*?" // 受害者ID
|
||
r"ejected from zone '([^']+)'.*?" // 原载具/区域
|
||
r"to zone '([^']+)'", // 目标区域
|
||
);
|
||
|
||
// 角色名称解析
|
||
static final _characterNamePattern = RegExp(r"name\s+([^-]+)");
|
||
|
||
// 本地库存请求解析
|
||
static final _requestLocationInventoryPattern = RegExp(r"Player\[([^\]]+)\].*?Location\[([^\]]+)\]");
|
||
|
||
/// 分析整个日志文件
|
||
///
|
||
/// [logFile] 日志文件
|
||
/// [startTime] 开始时间,如果提供则只统计此时间之后的数据
|
||
/// 返回日志分析结果列表和统计数据
|
||
static Future<(List<LogAnalyzeLineData>, LogAnalyzeStatistics)> analyzeLogFile(
|
||
File logFile, {
|
||
DateTime? startTime,
|
||
}) async {
|
||
if (!(await logFile.exists())) {
|
||
return (
|
||
[LogAnalyzeLineData(type: "error", title: S.current.log_analyzer_no_log_file)],
|
||
LogAnalyzeStatistics(
|
||
playerName: "",
|
||
killCount: 0,
|
||
deathCount: 0,
|
||
selfKillCount: 0,
|
||
vehicleDestructionCount: 0,
|
||
vehicleDestructionCountHard: 0,
|
||
gameCrashLineNumber: -1,
|
||
),
|
||
);
|
||
}
|
||
|
||
final logLines = utf8.decode((await logFile.readAsBytes()), allowMalformed: true).split("\n");
|
||
return _analyzeLogLines(logLines, startTime: startTime);
|
||
}
|
||
|
||
/// 分析日志行列表
|
||
///
|
||
/// [logLines] 日志行列表
|
||
/// [startTime] 开始时间,如果提供则只影响计数统计,不影响 gameStartTime 和位置的全量查找
|
||
/// 返回日志分析结果列表和统计数据
|
||
static (List<LogAnalyzeLineData>, LogAnalyzeStatistics) _analyzeLogLines(
|
||
List<String> logLines, {
|
||
DateTime? startTime,
|
||
}) {
|
||
final results = <LogAnalyzeLineData>[];
|
||
String playerName = "";
|
||
int killCount = 0;
|
||
int deathCount = 0;
|
||
int selfKillCount = 0;
|
||
int vehicleDestructionCount = 0;
|
||
int vehicleDestructionCountHard = 0;
|
||
DateTime? gameStartTime; // 全量查找,不受 startTime 影响
|
||
String? latestLocation; // 全量查找最新位置
|
||
int gameCrashLineNumber = -1;
|
||
bool shouldCount = startTime == null; // 只影响计数
|
||
|
||
for (var i = 0; i < logLines.length; i++) {
|
||
final line = logLines[i];
|
||
if (line.isEmpty) continue;
|
||
|
||
// 如果设置了 startTime,检查当前行时间
|
||
if (startTime != null && !shouldCount) {
|
||
final lineTime = _getLogLineDateTime(line);
|
||
if (lineTime != null && lineTime.isAfter(startTime)) {
|
||
shouldCount = true;
|
||
}
|
||
}
|
||
|
||
// 处理游戏开始(全量查找第一次出现)
|
||
if (gameStartTime == null) {
|
||
gameStartTime = _getLogLineDateTime(line);
|
||
if (gameStartTime != null) {
|
||
results.add(
|
||
LogAnalyzeLineData(
|
||
type: "info",
|
||
title: S.current.log_analyzer_game_start,
|
||
tag: "game_start", // 使用 tag 标识,不依赖本地化
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 游戏加载时间
|
||
final gameLoading = _parseGameLoading(line);
|
||
if (gameLoading != null) {
|
||
results.add(
|
||
LogAnalyzeLineData(
|
||
type: "info",
|
||
title: S.current.log_analyzer_game_loading,
|
||
data: S.current.log_analyzer_mode_loading_time(gameLoading.$1, gameLoading.$2),
|
||
dateTime: _getLogLineDateTimeString(line),
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
|
||
// 基础事件解析
|
||
final baseEvent = _parseBaseEvent(line);
|
||
if (baseEvent != null) {
|
||
LogAnalyzeLineData? data;
|
||
switch (baseEvent) {
|
||
case "AccountLoginCharacterStatus_Character":
|
||
data = _parseCharacterName(line);
|
||
if (data != null && data.playerName != null) {
|
||
playerName = data.playerName!; // 全量更新玩家名称
|
||
}
|
||
break;
|
||
case "FatalCollision":
|
||
data = _parseFatalCollision(line);
|
||
break;
|
||
case "Vehicle Destruction":
|
||
data = _parseVehicleDestruction(line, playerName, shouldCount, (isHard) {
|
||
if (isHard) {
|
||
vehicleDestructionCountHard++;
|
||
} else {
|
||
vehicleDestructionCount++;
|
||
}
|
||
});
|
||
break;
|
||
case "[ActorState] Dead":
|
||
data = _parseActorDeath(line, playerName, shouldCount, (isKill, isDeath, isSelfKill) {
|
||
if (isSelfKill) {
|
||
selfKillCount++;
|
||
} else {
|
||
if (isKill) killCount++;
|
||
if (isDeath) deathCount++;
|
||
}
|
||
});
|
||
break;
|
||
case "RequestLocationInventory":
|
||
data = _parseRequestLocationInventory(line);
|
||
if (data != null && data.location != null) {
|
||
latestLocation = data.location; // 全量更新最新位置
|
||
}
|
||
break;
|
||
}
|
||
if (data != null) {
|
||
results.add(data);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 游戏关闭
|
||
if (line.contains("[CIG] CCIGBroker::FastShutdown")) {
|
||
results.add(
|
||
LogAnalyzeLineData(
|
||
type: "info",
|
||
title: S.current.log_analyzer_game_close,
|
||
dateTime: _getLogLineDateTimeString(line),
|
||
),
|
||
);
|
||
continue;
|
||
}
|
||
|
||
// 游戏崩溃
|
||
if (line.contains("Cloud Imperium Games public crash handler")) {
|
||
gameCrashLineNumber = i;
|
||
}
|
||
}
|
||
|
||
// 处理崩溃信息
|
||
if (gameCrashLineNumber > 0) {
|
||
final lastLineDateTime = gameStartTime != null
|
||
? _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20")))
|
||
: null;
|
||
final crashInfo = logLines.sublist(gameCrashLineNumber);
|
||
final info = SCLoggerHelper.getGameRunningLogInfo(crashInfo);
|
||
crashInfo.add(S.current.log_analyzer_one_click_diagnosis_header);
|
||
if (info != null) {
|
||
crashInfo.add(info.key);
|
||
if (info.value.isNotEmpty) {
|
||
crashInfo.add(S.current.log_analyzer_details_info(info.value));
|
||
}
|
||
} else {
|
||
crashInfo.add(S.current.log_analyzer_no_crash_detected);
|
||
}
|
||
results.add(
|
||
LogAnalyzeLineData(
|
||
type: "game_crash",
|
||
title: S.current.log_analyzer_game_crash,
|
||
data: crashInfo.join("\n"),
|
||
dateTime: lastLineDateTime != null ? _dateTimeFormatter.format(lastLineDateTime) : null,
|
||
),
|
||
);
|
||
}
|
||
|
||
// 添加统计信息
|
||
if (killCount > 0 || deathCount > 0) {
|
||
results.add(
|
||
LogAnalyzeLineData(
|
||
type: "statistics",
|
||
title: S.current.log_analyzer_kill_summary,
|
||
data: S.current.log_analyzer_kill_death_suicide_count(
|
||
killCount,
|
||
deathCount,
|
||
selfKillCount,
|
||
vehicleDestructionCount,
|
||
vehicleDestructionCountHard,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// 统计游戏时长
|
||
if (gameStartTime != null) {
|
||
final lastLineDateTime = _getLogLineDateTime(logLines.lastWhere((e) => e.startsWith("<20"), orElse: () => ""));
|
||
if (lastLineDateTime != null) {
|
||
final duration = lastLineDateTime.difference(gameStartTime);
|
||
results.add(
|
||
LogAnalyzeLineData(
|
||
type: "statistics",
|
||
title: S.current.log_analyzer_play_time,
|
||
data: S.current.log_analyzer_play_time_format(
|
||
duration.inHours,
|
||
duration.inMinutes.remainder(60),
|
||
duration.inSeconds.remainder(60),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
final statistics = LogAnalyzeStatistics(
|
||
playerName: playerName,
|
||
killCount: killCount,
|
||
deathCount: deathCount,
|
||
selfKillCount: selfKillCount,
|
||
vehicleDestructionCount: vehicleDestructionCount,
|
||
vehicleDestructionCountHard: vehicleDestructionCountHard,
|
||
gameStartTime: gameStartTime,
|
||
gameCrashLineNumber: gameCrashLineNumber,
|
||
latestLocation: latestLocation,
|
||
);
|
||
|
||
return (results, statistics);
|
||
}
|
||
|
||
// ==================== 解析辅助方法 ====================
|
||
|
||
static String? _parseBaseEvent(String line) {
|
||
final match = _baseRegExp.firstMatch(line);
|
||
return match?.group(1);
|
||
}
|
||
|
||
static (String, String)? _parseGameLoading(String line) {
|
||
final match = _gameLoadingRegExp.firstMatch(line);
|
||
if (match != null) {
|
||
return (match.group(1) ?? "-", match.group(2) ?? "-");
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static DateTime? _getLogLineDateTime(String line) {
|
||
final match = _logDateTimeRegExp.firstMatch(line);
|
||
if (match != null) {
|
||
final dateTimeString = match.group(1);
|
||
if (dateTimeString != null) {
|
||
return DateTime.parse(dateTimeString).toLocal();
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static String? _getLogLineDateTimeString(String line) {
|
||
final dateTime = _getLogLineDateTime(line);
|
||
if (dateTime != null) {
|
||
return _dateTimeFormatter.format(dateTime);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static String? _safeExtract(RegExp pattern, String line) => pattern.firstMatch(line)?.group(1)?.trim();
|
||
|
||
static LogAnalyzeLineData? _parseFatalCollision(String line) {
|
||
final 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 distance = double.tryParse(_safeExtract(_fatalCollisionPatterns['distance']!, line) ?? '') ?? 0.0;
|
||
|
||
return LogAnalyzeLineData(
|
||
type: "fatal_collision",
|
||
title: S.current.log_analyzer_filter_fatal_collision,
|
||
data: S.current.log_analyzer_collision_details(
|
||
zone,
|
||
playerPilot ? '✅' : '❌',
|
||
hitEntity,
|
||
vehicle,
|
||
distance.toStringAsFixed(2),
|
||
),
|
||
dateTime: _getLogLineDateTimeString(line),
|
||
);
|
||
}
|
||
|
||
static LogAnalyzeLineData? _parseVehicleDestruction(
|
||
String line,
|
||
String playerName,
|
||
bool shouldCount,
|
||
void Function(bool isHard) onDestruction,
|
||
) {
|
||
final match = _vehicleDestructionPattern.firstMatch(line);
|
||
if (match != null) {
|
||
final vehicleModel = match.group(1) ?? unknownValue;
|
||
final zone = match.group(2) ?? unknownValue;
|
||
final destructionLevel = int.tryParse(match.group(3) ?? '') ?? 0;
|
||
final causedBy = match.group(4) ?? unknownValue;
|
||
|
||
final destructionLevelMap = {1: S.current.log_analyzer_soft_death, 2: S.current.log_analyzer_disintegration};
|
||
|
||
if (shouldCount && causedBy.trim() == playerName) {
|
||
onDestruction(destructionLevel == 2);
|
||
}
|
||
|
||
return LogAnalyzeLineData(
|
||
type: "vehicle_destruction",
|
||
title: S.current.log_analyzer_filter_vehicle_damaged,
|
||
data: S.current.log_analyzer_vehicle_damage_details(
|
||
vehicleModel,
|
||
zone,
|
||
destructionLevel.toString(),
|
||
destructionLevelMap[destructionLevel] ?? unknownValue,
|
||
causedBy,
|
||
),
|
||
dateTime: _getLogLineDateTimeString(line),
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static LogAnalyzeLineData? _parseActorDeath(
|
||
String line,
|
||
String playerName,
|
||
bool shouldCount,
|
||
void Function(bool isKill, bool isDeath, bool isSelfKill) onDeath,
|
||
) {
|
||
final match = _actorDeathPattern.firstMatch(line);
|
||
if (match != null) {
|
||
final victimId = match.group(1) ?? unknownValue;
|
||
final fromZone = match.group(2) ?? unknownValue;
|
||
final toZone = match.group(3) ?? unknownValue;
|
||
|
||
if (shouldCount) {
|
||
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, fromZone, toZone),
|
||
dateTime: _getLogLineDateTimeString(line),
|
||
location: fromZone,
|
||
area: toZone,
|
||
victimId: victimId,
|
||
playerName: playerName,
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static LogAnalyzeLineData? _parseCharacterName(String line) {
|
||
final match = _characterNamePattern.firstMatch(line);
|
||
if (match != null) {
|
||
final characterName = match.group(1)?.trim() ?? unknownValue;
|
||
return LogAnalyzeLineData(
|
||
type: "player_login",
|
||
title: S.current.log_analyzer_player_login(characterName),
|
||
dateTime: _getLogLineDateTimeString(line),
|
||
playerName: characterName, // 格式化字段
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
static LogAnalyzeLineData? _parseRequestLocationInventory(String line) {
|
||
final match = _requestLocationInventoryPattern.firstMatch(line);
|
||
if (match != null) {
|
||
final playerId = match.group(1) ?? unknownValue;
|
||
final location = match.group(2) ?? unknownValue;
|
||
|
||
return LogAnalyzeLineData(
|
||
type: "request_location_inventory",
|
||
title: S.current.log_analyzer_view_local_inventory,
|
||
data: S.current.log_analyzer_player_location(playerId, location),
|
||
dateTime: _getLogLineDateTimeString(line),
|
||
location: location, // 格式化字段
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
}
|