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