diff --git a/lib/common/helper/yearly_report_analyzer.dart b/lib/common/helper/yearly_report_analyzer.dart new file mode 100644 index 0000000..3cc5d08 --- /dev/null +++ b/lib/common/helper/yearly_report_analyzer.dart @@ -0,0 +1,522 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:starcitizen_doctor/common/helper/game_log_analyzer.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; + +/// 年度报告数据类 +class YearlyReportData { + // 基础统计 + final int totalLaunchCount; // 累计启动次数 + final Duration totalPlayTime; // 累计游玩时长 + final int yearlyLaunchCount; // 年度启动次数 + final Duration yearlyPlayTime; // 年度游玩时长 + final int totalCrashCount; // 总崩溃次数 + final int yearlyCrashCount; // 年度崩溃次数 + + // 时间统计 + final DateTime? yearlyFirstLaunchTime; // 年度第一次启动时间 + final DateTime? earliestPlayDate; // 年度游玩最早的一天 (05:00及以后) + final DateTime? latestPlayDate; // 年度游玩最晚的一天 (04:00及以前) + + // 载具统计 + final int yearlyVehicleDestructionCount; // 年度炸船次数 + final String? mostDestroyedVehicle; // 年度炸的最多的船 + final int mostDestroyedVehicleCount; // 炸的最多的船的次数 + final String? mostPilotedVehicle; // 年度最爱驾驶的载具 + final int mostPilotedVehicleCount; // 驾驶次数 + + // 战斗统计 + final int yearlyKillCount; // 年度击杀次数 + final int yearlyDeathCount; // 年度死亡次数 + + // 账号统计 + final int accountCount; // 账号数量 + final String? mostPlayedAccount; // 游玩最多的账号 + final int mostPlayedAccountSessionCount; // 游玩最多的账号的会话次数 + + // 详细数据 (用于展示) + final Map vehicleDestructionDetails; // 载具损毁详情 + final Map vehiclePilotedDetails; // 驾驶载具详情 + final Map accountSessionDetails; // 账号会话详情 + + const YearlyReportData({ + required this.totalLaunchCount, + required this.totalPlayTime, + required this.yearlyLaunchCount, + required this.yearlyPlayTime, + required this.totalCrashCount, + required this.yearlyCrashCount, + this.yearlyFirstLaunchTime, + this.earliestPlayDate, + this.latestPlayDate, + required this.yearlyVehicleDestructionCount, + this.mostDestroyedVehicle, + required this.mostDestroyedVehicleCount, + this.mostPilotedVehicle, + required this.mostPilotedVehicleCount, + required this.yearlyKillCount, + required this.yearlyDeathCount, + required this.accountCount, + this.mostPlayedAccount, + required this.mostPlayedAccountSessionCount, + required this.vehicleDestructionDetails, + required this.vehiclePilotedDetails, + required this.accountSessionDetails, + }); + + /// 将 DateTime 转换为带时区的 ISO 8601 字符串 + /// 输出格式: 2025-12-17T10:30:00.000+08:00 + static String? _toIso8601WithTimezone(DateTime? dateTime) { + if (dateTime == null) return null; + final local = dateTime.toLocal(); + final offset = local.timeZoneOffset; + final sign = offset.isNegative ? '-' : '+'; + final hours = offset.inHours.abs().toString().padLeft(2, '0'); + final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); + // 使用本地时间的 ISO 字符串,然后附加时区偏移 + final isoString = local.toIso8601String(); + // 移除可能的 'Z' 后缀(UTC 标记) + final baseString = isoString.endsWith('Z') ? isoString.substring(0, isoString.length - 1) : isoString; + return '$baseString$sign$hours:$minutes'; + } + + /// 转换为 JSON Map + Map toJson() { + final now = DateTime.now(); + final offset = now.timeZoneOffset; + final sign = offset.isNegative ? '-' : '+'; + final hours = offset.inHours.abs().toString().padLeft(2, '0'); + final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); + + return { + // 元数据 + 'generatedAt': _toIso8601WithTimezone(now), + 'timezoneOffset': '$sign$hours:$minutes', + 'timezoneOffsetMinutes': offset.inMinutes, + + // 基础统计 + 'totalLaunchCount': totalLaunchCount, + 'totalPlayTimeMs': totalPlayTime.inMilliseconds, + 'yearlyLaunchCount': yearlyLaunchCount, + 'yearlyPlayTimeMs': yearlyPlayTime.inMilliseconds, + 'totalCrashCount': totalCrashCount, + 'yearlyCrashCount': yearlyCrashCount, + + // 时间统计 (带时区) + 'yearlyFirstLaunchTime': _toIso8601WithTimezone(yearlyFirstLaunchTime), + 'earliestPlayDate': _toIso8601WithTimezone(earliestPlayDate), + 'latestPlayDate': _toIso8601WithTimezone(latestPlayDate), + + // 载具统计 + 'yearlyVehicleDestructionCount': yearlyVehicleDestructionCount, + 'mostDestroyedVehicle': mostDestroyedVehicle, + 'mostDestroyedVehicleCount': mostDestroyedVehicleCount, + 'mostPilotedVehicle': mostPilotedVehicle, + 'mostPilotedVehicleCount': mostPilotedVehicleCount, + + // 战斗统计 + 'yearlyKillCount': yearlyKillCount, + 'yearlyDeathCount': yearlyDeathCount, + + // 账号统计 + 'accountCount': accountCount, + 'mostPlayedAccount': mostPlayedAccount, + 'mostPlayedAccountSessionCount': mostPlayedAccountSessionCount, + + // 详细数据 + 'vehicleDestructionDetails': vehicleDestructionDetails, + 'vehiclePilotedDetails': vehiclePilotedDetails, + 'accountSessionDetails': accountSessionDetails, + }; + } + + /// 转换为 JSON 字符串 + String toJsonString() => jsonEncode(toJson()); + + /// 转换为 Base64 编码的 JSON 字符串 + String toJsonBase64() => base64Encode(utf8.encode(toJsonString())); + + @override + String toString() { + return '''YearlyReportData( + totalLaunchCount: $totalLaunchCount, + totalPlayTime: $totalPlayTime, + yearlyLaunchCount: $yearlyLaunchCount, + yearlyPlayTime: $yearlyPlayTime, + totalCrashCount: $totalCrashCount, + yearlyCrashCount: $yearlyCrashCount, + yearlyFirstLaunchTime: $yearlyFirstLaunchTime, + earliestPlayDate: $earliestPlayDate, + latestPlayDate: $latestPlayDate, + yearlyVehicleDestructionCount: $yearlyVehicleDestructionCount, + mostDestroyedVehicle: $mostDestroyedVehicle ($mostDestroyedVehicleCount), + mostPilotedVehicle: $mostPilotedVehicle ($mostPilotedVehicleCount), + yearlyKillCount: $yearlyKillCount, + yearlyDeathCount: $yearlyDeathCount, + accountCount: $accountCount, + mostPlayedAccount: $mostPlayedAccount ($mostPlayedAccountSessionCount), +)'''; + } +} + +/// 单个日志文件的统计结果 (内部使用) +class _LogFileStats { + DateTime? startTime; + DateTime? endTime; + bool hasCrash = false; + int killCount = 0; + int deathCount = 0; + Set playerNames = {}; + String? currentPlayerName; + String? firstPlayerName; // 第一个检测到的玩家名,用于去重 + + // 载具损毁: 载具型号 (去除ID后) -> 次数 + Map vehicleDestruction = {}; + + // 驾驶载具: 载具型号 (去除ID后) -> 次数 + Map vehiclePiloted = {}; + + // 年度内的时间记录 + List yearlyStartTimes = []; + List yearlyEndTimes = []; + + /// 生成用于去重的唯一标识 + /// 基于启动时间和第一个玩家名生成 + String? get uniqueKey { + if (startTime == null) return null; + final timeKey = startTime!.toUtc().toIso8601String(); + final playerKey = firstPlayerName ?? 'unknown'; + return '$timeKey|$playerKey'; + } +} + +/// 年度报告分析器 +class YearlyReportAnalyzer { + // 复用 GameLogAnalyzer 中的正则表达式和方法 + static final _characterNamePattern = RegExp(r"name\s+([^-]+)"); + 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 Future<_LogFileStats> _analyzeLogFile(File logFile, int targetYear) async { + final stats = _LogFileStats(); + + if (!(await logFile.exists())) { + return stats; + } + + try { + final content = utf8.decode(await logFile.readAsBytes(), allowMalformed: true); + final lines = content.split('\n'); + + for (final line in lines) { + if (line.isEmpty) continue; + + final lineTime = GameLogAnalyzer.getLogLineDateTime(line); + + // 记录开始时间 (第一个有效时间) + if (stats.startTime == null && lineTime != null) { + stats.startTime = lineTime; + } + + // 更新结束时间 (最后一个有效时间) + if (lineTime != null) { + stats.endTime = lineTime; + + // 记录年度内的时间 + if (lineTime.year == targetYear) { + if (stats.yearlyStartTimes.isEmpty || + stats.yearlyStartTimes.last.difference(lineTime).abs().inMinutes > 30) { + // 新的会话开始 + stats.yearlyStartTimes.add(lineTime); + } + // 总是更新最后的结束时间 + if (stats.yearlyEndTimes.isEmpty) { + stats.yearlyEndTimes.add(lineTime); + } else { + stats.yearlyEndTimes[stats.yearlyEndTimes.length - 1] = lineTime; + } + } + } + + // 检测崩溃 + if (line.contains("Cloud Imperium Games public crash handler")) { + stats.hasCrash = true; + } + + // 检测玩家登录 + final nameMatch = _characterNamePattern.firstMatch(line); + if (nameMatch != null) { + final playerName = nameMatch.group(1)?.trim(); + if (playerName != null && playerName.isNotEmpty) { + stats.currentPlayerName = playerName; + stats.playerNames.add(playerName); + // 记录第一个玩家名用于去重 + stats.firstPlayerName ??= playerName; + } + } + + // 检测载具损毁 (仅年度内) + if (lineTime != null && lineTime.year == targetYear) { + final destructionMatch = _vehicleDestructionPattern.firstMatch(line); + if (destructionMatch != null) { + final vehicleModel = destructionMatch.group(1); + final causedBy = destructionMatch.group(4)?.trim(); + + if (vehicleModel != null && + causedBy != null && + stats.currentPlayerName != null && + causedBy == stats.currentPlayerName) { + final cleanVehicleName = GameLogAnalyzer.removeVehicleId(vehicleModel); + stats.vehicleDestruction[cleanVehicleName] = (stats.vehicleDestruction[cleanVehicleName] ?? 0) + 1; + } + } + + // 检测驾驶载具 + final controlMatch = GameLogAnalyzer.vehicleControlPattern.firstMatch(line); + if (controlMatch != null) { + final vehicleName = controlMatch.group(1); + if (vehicleName != null) { + final cleanVehicleName = GameLogAnalyzer.removeVehicleId(vehicleName); + stats.vehiclePiloted[cleanVehicleName] = (stats.vehiclePiloted[cleanVehicleName] ?? 0) + 1; + } + } + + // 检测死亡 + final deathMatch = _actorDeathPattern.firstMatch(line); + if (deathMatch != null) { + final victimId = deathMatch.group(1)?.trim(); + + if (victimId != null && stats.currentPlayerName != null && victimId == stats.currentPlayerName) { + stats.deathCount++; + } + } + } + } + } catch (e) { + dPrint('[YearlyReportAnalyzer] Error analyzing log file: $e'); + } + + return stats; + } + + /// 生成年度报告 + /// + /// [gameInstallPaths] 游戏安装路径列表 (完整路径,如 ["D:/Games/StarCitizen/LIVE", "D:/Games/StarCitizen/PTU"]) + /// [targetYear] 目标年份 + static Future generateReport(List gameInstallPaths, int targetYear) async { + final List allLogFiles = []; + + // 从所有安装路径收集日志文件 + for (final installPath in gameInstallPaths) { + final installDir = Directory(installPath); + + // 检查安装目录是否存在 + if (!await installDir.exists()) { + dPrint('[YearlyReportAnalyzer] Install path does not exist: $installPath'); + continue; + } + + final gameLogFile = File('$installPath/Game.log'); + final logBackupsDir = Directory('$installPath/logbackups'); + + // 添加当前 Game.log + if (await gameLogFile.exists()) { + allLogFiles.add(gameLogFile); + } + + // 添加备份日志 + if (await logBackupsDir.exists()) { + await for (final entity in logBackupsDir.list()) { + if (entity is File && entity.path.endsWith('.log')) { + allLogFiles.add(entity); + } + } + } + } + + dPrint( + '[YearlyReportAnalyzer] Found ${allLogFiles.length} log files from ${gameInstallPaths.length} install paths', + ); + + // 并发分析所有日志文件 + final futures = allLogFiles.map((file) => _analyzeLogFile(file, targetYear)); + final allStatsRaw = await Future.wait(futures); + + // 去重: 使用 uniqueKey (启动时间 + 玩家名) 来过滤重复的日志 + final seenKeys = {}; + final allStats = <_LogFileStats>[]; + + for (final stats in allStatsRaw) { + final key = stats.uniqueKey; + if (key == null) { + // 无法生成唯一标识的日志仍然保留 + allStats.add(stats); + } else if (!seenKeys.contains(key)) { + seenKeys.add(key); + allStats.add(stats); + } else { + dPrint('[YearlyReportAnalyzer] Skipping duplicate log: $key'); + } + } + + dPrint('[YearlyReportAnalyzer] After deduplication: ${allStats.length} unique logs'); + + // 合并统计数据 + int totalLaunchCount = allStats.length; + Duration totalPlayTime = Duration.zero; + int yearlyLaunchCount = 0; + Duration yearlyPlayTime = Duration.zero; + int totalCrashCount = 0; + int yearlyCrashCount = 0; + DateTime? yearlyFirstLaunchTime; + DateTime? earliestPlayDate; + DateTime? latestPlayDate; + int yearlyKillCount = 0; + int yearlyDeathCount = 0; + + final Map vehicleDestructionDetails = {}; + final Map vehiclePilotedDetails = {}; + final Map accountSessionDetails = {}; + + for (final stats in allStats) { + // 累计游玩时长 + if (stats.startTime != null && stats.endTime != null) { + totalPlayTime += stats.endTime!.difference(stats.startTime!); + } + + // 崩溃统计 + if (stats.hasCrash) { + totalCrashCount++; + // 检查是否为年度内的崩溃 + if (stats.endTime != null && stats.endTime!.year == targetYear) { + yearlyCrashCount++; + } + } + + // 年度统计 + for (int i = 0; i < stats.yearlyStartTimes.length; i++) { + yearlyLaunchCount++; + final startTime = stats.yearlyStartTimes[i]; + final endTime = i < stats.yearlyEndTimes.length ? stats.yearlyEndTimes[i] : startTime; + yearlyPlayTime += endTime.difference(startTime); + + // 年度第一次启动时间 + if (yearlyFirstLaunchTime == null || startTime.isBefore(yearlyFirstLaunchTime)) { + yearlyFirstLaunchTime = startTime; + } + + // 最早游玩的一天 (05:00及以后开始游戏) + if (startTime.hour >= 5) { + if (earliestPlayDate == null || _timeOfDayIsEarlier(startTime, earliestPlayDate)) { + earliestPlayDate = startTime; + } + } + + // 最晚游玩的一天 (04:00及以前结束游戏) + if (endTime.hour <= 4) { + if (latestPlayDate == null || _timeOfDayIsLater(endTime, latestPlayDate)) { + latestPlayDate = endTime; + } + } + } + + // 累加战斗统计 + yearlyKillCount += stats.killCount; + yearlyDeathCount += stats.deathCount; + + // 合并载具损毁详情 + for (final entry in stats.vehicleDestruction.entries) { + vehicleDestructionDetails[entry.key] = (vehicleDestructionDetails[entry.key] ?? 0) + entry.value; + } + + // 合并驾驶载具详情 + for (final entry in stats.vehiclePiloted.entries) { + vehiclePilotedDetails[entry.key] = (vehiclePilotedDetails[entry.key] ?? 0) + entry.value; + } + + // 合并账号会话详情 + for (final playerName in stats.playerNames) { + accountSessionDetails[playerName] = (accountSessionDetails[playerName] ?? 0) + 1; + } + } + + // 计算派生统计 + final yearlyVehicleDestructionCount = vehicleDestructionDetails.values.fold(0, (a, b) => a + b); + + String? mostDestroyedVehicle; + int mostDestroyedVehicleCount = 0; + for (final entry in vehicleDestructionDetails.entries) { + if (entry.value > mostDestroyedVehicleCount) { + mostDestroyedVehicle = entry.key; + mostDestroyedVehicleCount = entry.value; + } + } + + String? mostPilotedVehicle; + int mostPilotedVehicleCount = 0; + for (final entry in vehiclePilotedDetails.entries) { + if (entry.value > mostPilotedVehicleCount) { + mostPilotedVehicle = entry.key; + mostPilotedVehicleCount = entry.value; + } + } + + String? mostPlayedAccount; + int mostPlayedAccountSessionCount = 0; + for (final entry in accountSessionDetails.entries) { + if (entry.value > mostPlayedAccountSessionCount) { + mostPlayedAccount = entry.key; + mostPlayedAccountSessionCount = entry.value; + } + } + + return YearlyReportData( + totalLaunchCount: totalLaunchCount, + totalPlayTime: totalPlayTime, + yearlyLaunchCount: yearlyLaunchCount, + yearlyPlayTime: yearlyPlayTime, + totalCrashCount: totalCrashCount, + yearlyCrashCount: yearlyCrashCount, + yearlyFirstLaunchTime: yearlyFirstLaunchTime, + earliestPlayDate: earliestPlayDate, + latestPlayDate: latestPlayDate, + yearlyVehicleDestructionCount: yearlyVehicleDestructionCount, + mostDestroyedVehicle: mostDestroyedVehicle, + mostDestroyedVehicleCount: mostDestroyedVehicleCount, + mostPilotedVehicle: mostPilotedVehicle, + mostPilotedVehicleCount: mostPilotedVehicleCount, + yearlyKillCount: yearlyKillCount, + yearlyDeathCount: yearlyDeathCount, + accountCount: accountSessionDetails.length, + mostPlayedAccount: mostPlayedAccount, + mostPlayedAccountSessionCount: mostPlayedAccountSessionCount, + vehicleDestructionDetails: vehicleDestructionDetails, + vehiclePilotedDetails: vehiclePilotedDetails, + accountSessionDetails: accountSessionDetails, + ); + } + + /// 比较两个时间的 时:分 是否更早 + static bool _timeOfDayIsEarlier(DateTime a, DateTime b) { + if (a.hour < b.hour) return true; + if (a.hour > b.hour) return false; + return a.minute < b.minute; + } + + /// 比较两个时间的 时:分 是否更晚 + static bool _timeOfDayIsLater(DateTime a, DateTime b) { + if (a.hour > b.hour) return true; + if (a.hour < b.hour) return false; + return a.minute > b.minute; + } +}