494 lines
18 KiB
C#
494 lines
18 KiB
C#
/*
|
|
* @Author: Codex
|
|
* @Description: 玩家主动 Bug 汇报打包服务
|
|
* @Date: 2026年05月19日
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Logic.Config;
|
|
using Logic.CrashSight;
|
|
using Logic.Multilingual;
|
|
using RuntimeData;
|
|
using TH1_Logic.Config;
|
|
using TH1_Logic.GameArchive;
|
|
using UnityEngine;
|
|
|
|
|
|
namespace TH1_Logic.Oss
|
|
{
|
|
public enum PlayerBugArchiveKind
|
|
{
|
|
Begin,
|
|
Continue,
|
|
End
|
|
}
|
|
|
|
[Serializable]
|
|
public class PlayerBugReportArchiveManifest
|
|
{
|
|
public string mode;
|
|
public uint mapId;
|
|
public string kind;
|
|
public string archiveFolder;
|
|
public string sourceFileName;
|
|
public string zipEntry;
|
|
public long fileSize;
|
|
public string lastWriteTimeUtc;
|
|
}
|
|
|
|
[Serializable]
|
|
public class PlayerBugReportManifest
|
|
{
|
|
public string schema = "th1.player-bug-report.v1";
|
|
public string reportId;
|
|
public string createdAtUtc;
|
|
public string createdAtLocal;
|
|
public string timezone;
|
|
public string steamId;
|
|
public string version;
|
|
public string unityVersion;
|
|
public string platform;
|
|
public string crashSightDeviceId;
|
|
public string deviceModel;
|
|
public string deviceName;
|
|
public string operatingSystem;
|
|
public string processorType;
|
|
public int processorCount;
|
|
public int systemMemorySizeMb;
|
|
public string graphicsDeviceName;
|
|
public int graphicsMemorySizeMb;
|
|
public string description;
|
|
public int archiveCount;
|
|
public PlayerBugReportArchiveManifest[] archives = Array.Empty<PlayerBugReportArchiveManifest>();
|
|
}
|
|
|
|
public class PlayerBugReportArchiveSession
|
|
{
|
|
public bool IsMulti;
|
|
public uint MapId;
|
|
public string BeginPath;
|
|
public string CompanionPath;
|
|
public PlayerBugArchiveKind CompanionKind;
|
|
public DateTime LatestWriteTimeUtc;
|
|
|
|
public string ModeLabel => IsMulti ? "multi" : "single";
|
|
public string ModeDisplay => IsMulti ? "联机" : "单机";
|
|
public string CompanionKindLabel => CompanionKind == PlayerBugArchiveKind.End ? "end" : "continue";
|
|
|
|
public long TotalBytes
|
|
{
|
|
get
|
|
{
|
|
long total = 0;
|
|
if (!string.IsNullOrEmpty(BeginPath) && File.Exists(BeginPath))
|
|
total += new FileInfo(BeginPath).Length;
|
|
if (!string.IsNullOrEmpty(CompanionPath) && File.Exists(CompanionPath))
|
|
total += new FileInfo(CompanionPath).Length;
|
|
return total;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class PlayerBugReportPackage
|
|
{
|
|
public byte[] Data;
|
|
public PlayerBugReportManifest Manifest;
|
|
public List<PlayerBugReportArchiveSession> Sessions;
|
|
}
|
|
|
|
[Serializable]
|
|
public class PlayerMultilingualReportManifest
|
|
{
|
|
public string schema = "th1.player-multilingual-report.v1";
|
|
public string reportId;
|
|
public string createdAtUtc;
|
|
public string createdAtLocal;
|
|
public string timezone;
|
|
public string steamId;
|
|
public string version;
|
|
public string unityVersion;
|
|
public string platform;
|
|
public string crashSightDeviceId;
|
|
public string deviceModel;
|
|
public string deviceName;
|
|
public string operatingSystem;
|
|
public string processorType;
|
|
public int processorCount;
|
|
public int systemMemorySizeMb;
|
|
public string graphicsDeviceName;
|
|
public int graphicsMemorySizeMb;
|
|
public uint multilingualId;
|
|
public string multilingualIdText;
|
|
public string language;
|
|
public string reportedText;
|
|
public string description;
|
|
public string resolvedText;
|
|
}
|
|
|
|
public class PlayerMultilingualReportPackage
|
|
{
|
|
public byte[] Data;
|
|
public PlayerMultilingualReportManifest Manifest;
|
|
}
|
|
|
|
public static class PlayerMultilingualReportService
|
|
{
|
|
public const int MaxMultilingualReportUploadBytes = 1 * 1024 * 1024;
|
|
|
|
private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(false);
|
|
|
|
public static string GetCurrentVersion()
|
|
{
|
|
try
|
|
{
|
|
if (ConfigManager.Instance.VersionCfg == null)
|
|
ConfigManager.Instance.VersionCfg = TH1Resource.ResourceLoader.Load<VersionConfig>("Export/VersionConfig");
|
|
|
|
return ConfigManager.Instance.VersionCfg?.CurVersionInfo?.Version ?? Application.version;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return Application.version;
|
|
}
|
|
}
|
|
|
|
public static MultilingualType GetCurrentLanguage()
|
|
{
|
|
try
|
|
{
|
|
var current = MultilingualManager.Instance.CurrentType;
|
|
if (IsValidReportLanguage(current)) return current;
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
try
|
|
{
|
|
var configured = ConfigManager.Instance.Config.MultilingualType;
|
|
if (IsValidReportLanguage(configured)) return configured;
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
return MultilingualType.EN;
|
|
}
|
|
|
|
public static string ResolveText(uint multilingualId, MultilingualType language)
|
|
{
|
|
if (multilingualId == 0) return "";
|
|
language = IsValidReportLanguage(language) ? language : GetCurrentLanguage();
|
|
|
|
try
|
|
{
|
|
return MultilingualManager.Instance.GetMultilingualText(multilingualId, language) ?? "";
|
|
}
|
|
catch
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
public static PlayerMultilingualReportPackage BuildPackage(string steamId, uint multilingualId,
|
|
string reportedText, string description, string version, MultilingualType language)
|
|
{
|
|
language = IsValidReportLanguage(language) ? language : GetCurrentLanguage();
|
|
var reportId = Guid.NewGuid().ToString("N");
|
|
var manifest = new PlayerMultilingualReportManifest
|
|
{
|
|
reportId = reportId,
|
|
createdAtUtc = DateTime.UtcNow.ToString("O"),
|
|
createdAtLocal = DateTime.Now.ToString("O"),
|
|
timezone = GetLocalTimezone(),
|
|
steamId = steamId ?? "",
|
|
version = string.IsNullOrWhiteSpace(version) ? GetCurrentVersion() : version.Trim(),
|
|
unityVersion = Application.unityVersion,
|
|
platform = Application.platform.ToString(),
|
|
crashSightDeviceId = CrashSightManager.GetCrashSightDeviceId(),
|
|
deviceModel = SystemInfo.deviceModel,
|
|
deviceName = SystemInfo.deviceName,
|
|
operatingSystem = SystemInfo.operatingSystem,
|
|
processorType = SystemInfo.processorType,
|
|
processorCount = SystemInfo.processorCount,
|
|
systemMemorySizeMb = SystemInfo.systemMemorySize,
|
|
graphicsDeviceName = SystemInfo.graphicsDeviceName,
|
|
graphicsMemorySizeMb = SystemInfo.graphicsMemorySize,
|
|
multilingualId = multilingualId,
|
|
multilingualIdText = multilingualId.ToString(),
|
|
language = language.ToString(),
|
|
reportedText = reportedText ?? "",
|
|
description = description ?? "",
|
|
resolvedText = ResolveText(multilingualId, language)
|
|
};
|
|
|
|
using var stream = new MemoryStream();
|
|
using (var zip = new ZipArchive(stream, ZipArchiveMode.Create, true, Utf8NoBom))
|
|
{
|
|
WriteTextEntry(zip, "reported_text.txt", manifest.reportedText);
|
|
WriteTextEntry(zip, "description.txt", manifest.description);
|
|
WriteTextEntry(zip, "manifest.json", JsonUtility.ToJson(manifest, true));
|
|
}
|
|
|
|
return new PlayerMultilingualReportPackage
|
|
{
|
|
Data = stream.ToArray(),
|
|
Manifest = manifest
|
|
};
|
|
}
|
|
|
|
private static void WriteTextEntry(ZipArchive zip, string path, string text)
|
|
{
|
|
var entry = zip.CreateEntry(path, System.IO.Compression.CompressionLevel.Optimal);
|
|
using var entryStream = entry.Open();
|
|
var bytes = Utf8NoBom.GetBytes(text ?? "");
|
|
entryStream.Write(bytes, 0, bytes.Length);
|
|
}
|
|
|
|
private static bool IsValidReportLanguage(MultilingualType language)
|
|
{
|
|
return language != MultilingualType.None && language != MultilingualType.Max;
|
|
}
|
|
|
|
private static string GetLocalTimezone()
|
|
{
|
|
try
|
|
{
|
|
var offset = DateTimeOffset.Now.Offset;
|
|
var sign = offset < TimeSpan.Zero ? "-" : "+";
|
|
return $"{TimeZoneInfo.Local.Id} (UTC{sign}{offset.Duration():hh\\:mm})";
|
|
}
|
|
catch
|
|
{
|
|
return DateTimeOffset.Now.Offset.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class PlayerBugReportService
|
|
{
|
|
public const int MaxBugReportUploadBytes = 10 * 1024 * 1024;
|
|
|
|
private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(false);
|
|
|
|
public static string ConfigDirectory =>
|
|
Path.GetFullPath(Path.Combine(Application.persistentDataPath, "../Config"));
|
|
|
|
public static string GetCurrentVersion()
|
|
{
|
|
try
|
|
{
|
|
if (ConfigManager.Instance.VersionCfg == null)
|
|
ConfigManager.Instance.VersionCfg = TH1Resource.ResourceLoader.Load<VersionConfig>("Export/VersionConfig");
|
|
|
|
return ConfigManager.Instance.VersionCfg?.CurVersionInfo?.Version ?? Application.version;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return Application.version;
|
|
}
|
|
}
|
|
|
|
public static PlayerBugReportArchiveSession GetMostRecentArchiveSession()
|
|
{
|
|
return GetLatestArchiveSessions(true, true)
|
|
.OrderByDescending(session => session.LatestWriteTimeUtc)
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
public static List<PlayerBugReportArchiveSession> GetLatestArchiveSessions(bool includeSingle, bool includeMulti)
|
|
{
|
|
var sessions = new List<PlayerBugReportArchiveSession>();
|
|
if (includeSingle && TryGetLatestArchiveSession(false, out var singleSession))
|
|
sessions.Add(singleSession);
|
|
if (includeMulti && TryGetLatestArchiveSession(true, out var multiSession))
|
|
sessions.Add(multiSession);
|
|
|
|
sessions.Sort((a, b) => b.LatestWriteTimeUtc.CompareTo(a.LatestWriteTimeUtc));
|
|
return sessions;
|
|
}
|
|
|
|
public static PlayerBugReportPackage BuildPackage(string steamId, string description, string version,
|
|
bool includeArchives, bool includeSingleArchive, bool includeMultiArchive)
|
|
{
|
|
var sessions = includeArchives
|
|
? GetLatestArchiveSessions(includeSingleArchive, includeMultiArchive)
|
|
: new List<PlayerBugReportArchiveSession>();
|
|
|
|
var reportId = Guid.NewGuid().ToString("N");
|
|
var manifestArchives = new List<PlayerBugReportArchiveManifest>();
|
|
var manifest = new PlayerBugReportManifest
|
|
{
|
|
reportId = reportId,
|
|
createdAtUtc = DateTime.UtcNow.ToString("O"),
|
|
createdAtLocal = DateTime.Now.ToString("O"),
|
|
timezone = GetLocalTimezone(),
|
|
steamId = steamId ?? "",
|
|
version = string.IsNullOrWhiteSpace(version) ? GetCurrentVersion() : version.Trim(),
|
|
unityVersion = Application.unityVersion,
|
|
platform = Application.platform.ToString(),
|
|
crashSightDeviceId = CrashSightManager.GetCrashSightDeviceId(),
|
|
deviceModel = SystemInfo.deviceModel,
|
|
deviceName = SystemInfo.deviceName,
|
|
operatingSystem = SystemInfo.operatingSystem,
|
|
processorType = SystemInfo.processorType,
|
|
processorCount = SystemInfo.processorCount,
|
|
systemMemorySizeMb = SystemInfo.systemMemorySize,
|
|
graphicsDeviceName = SystemInfo.graphicsDeviceName,
|
|
graphicsMemorySizeMb = SystemInfo.graphicsMemorySize,
|
|
description = description ?? ""
|
|
};
|
|
|
|
using var stream = new MemoryStream();
|
|
using (var zip = new ZipArchive(stream, ZipArchiveMode.Create, true, Utf8NoBom))
|
|
{
|
|
WriteTextEntry(zip, "description.txt", manifest.description);
|
|
|
|
foreach (var session in sessions)
|
|
{
|
|
AddArchiveFile(zip, session, PlayerBugArchiveKind.Begin, session.BeginPath, manifestArchives);
|
|
AddArchiveFile(zip, session, session.CompanionKind, session.CompanionPath, manifestArchives);
|
|
}
|
|
|
|
manifest.archiveCount = manifestArchives.Count;
|
|
manifest.archives = manifestArchives.ToArray();
|
|
WriteTextEntry(zip, "manifest.json", JsonUtility.ToJson(manifest, true));
|
|
}
|
|
|
|
return new PlayerBugReportPackage
|
|
{
|
|
Data = stream.ToArray(),
|
|
Manifest = manifest,
|
|
Sessions = sessions
|
|
};
|
|
}
|
|
|
|
private static bool TryGetLatestArchiveSession(bool isMulti, out PlayerBugReportArchiveSession session)
|
|
{
|
|
session = null;
|
|
var expectedMode = isMulti ? NetMode.Multi : NetMode.Single;
|
|
var records = GameRecordManager.Instance.GameRecordData?.Records ?? new List<GameRecord>();
|
|
session = records
|
|
.Where(record => record != null && record.NetMode == expectedMode)
|
|
.Select(TryBuildArchiveSession)
|
|
.Where(value => value != null)
|
|
.OrderByDescending(value => value.LatestWriteTimeUtc)
|
|
.FirstOrDefault();
|
|
return session != null;
|
|
}
|
|
|
|
private static PlayerBugReportArchiveSession TryBuildArchiveSession(GameRecord record)
|
|
{
|
|
if (record == null || string.IsNullOrEmpty(record.BeginArchiveId)) return null;
|
|
if (!GameArchiveManager.Instance.TryGetArchivePath(
|
|
GameArchiveFileKind.Begin,
|
|
record.BeginArchiveId,
|
|
out var beginPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var companionKind = PlayerBugArchiveKind.End;
|
|
var companionArchiveKind = GameArchiveFileKind.End;
|
|
var companionArchiveId = record.EndArchiveId;
|
|
if (string.IsNullOrEmpty(companionArchiveId))
|
|
{
|
|
companionKind = PlayerBugArchiveKind.Continue;
|
|
companionArchiveKind = record.RecordKind == GameRecordKind.Quick
|
|
? GameArchiveFileKind.QuickContinue
|
|
: GameArchiveFileKind.Continue;
|
|
companionArchiveId = record.ContinueArchiveId;
|
|
}
|
|
if (string.IsNullOrEmpty(companionArchiveId)) return null;
|
|
if (!GameArchiveManager.Instance.TryGetArchivePath(
|
|
companionArchiveKind,
|
|
companionArchiveId,
|
|
out var companionPath))
|
|
return null;
|
|
|
|
return new PlayerBugReportArchiveSession
|
|
{
|
|
IsMulti = record.NetMode == NetMode.Multi,
|
|
MapId = record.MapID,
|
|
BeginPath = beginPath,
|
|
CompanionPath = companionPath,
|
|
CompanionKind = companionKind,
|
|
LatestWriteTimeUtc = File.GetLastWriteTimeUtc(beginPath) > File.GetLastWriteTimeUtc(companionPath)
|
|
? File.GetLastWriteTimeUtc(beginPath)
|
|
: File.GetLastWriteTimeUtc(companionPath)
|
|
};
|
|
}
|
|
|
|
private static void AddArchiveFile(ZipArchive zip, PlayerBugReportArchiveSession session,
|
|
PlayerBugArchiveKind kind, string sourcePath, List<PlayerBugReportArchiveManifest> manifestArchives)
|
|
{
|
|
if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath)) return;
|
|
|
|
var kindLabel = GetKindLabel(kind);
|
|
var sourceFileName = Path.GetFileName(sourcePath);
|
|
var archiveFolder = Path.GetFileName(Path.GetDirectoryName(sourcePath)) ?? "";
|
|
var entryPath = string.IsNullOrEmpty(archiveFolder)
|
|
? $"saves/{session.ModeLabel}/{sourceFileName}"
|
|
: $"saves/{session.ModeLabel}/{archiveFolder}/{sourceFileName}";
|
|
var entry = zip.CreateEntry(entryPath, System.IO.Compression.CompressionLevel.Optimal);
|
|
using (var entryStream = entry.Open())
|
|
using (var fileStream = File.OpenRead(sourcePath))
|
|
{
|
|
fileStream.CopyTo(entryStream);
|
|
}
|
|
|
|
var fileInfo = new FileInfo(sourcePath);
|
|
manifestArchives.Add(new PlayerBugReportArchiveManifest
|
|
{
|
|
mode = session.ModeLabel,
|
|
mapId = session.MapId,
|
|
kind = kindLabel,
|
|
archiveFolder = archiveFolder,
|
|
sourceFileName = sourceFileName,
|
|
zipEntry = entryPath,
|
|
fileSize = fileInfo.Length,
|
|
lastWriteTimeUtc = fileInfo.LastWriteTimeUtc.ToString("O")
|
|
});
|
|
}
|
|
|
|
private static void WriteTextEntry(ZipArchive zip, string path, string text)
|
|
{
|
|
var entry = zip.CreateEntry(path, System.IO.Compression.CompressionLevel.Optimal);
|
|
using var entryStream = entry.Open();
|
|
var bytes = Utf8NoBom.GetBytes(text ?? "");
|
|
entryStream.Write(bytes, 0, bytes.Length);
|
|
}
|
|
|
|
private static string GetKindLabel(PlayerBugArchiveKind kind)
|
|
{
|
|
return kind switch
|
|
{
|
|
PlayerBugArchiveKind.Begin => "begin",
|
|
PlayerBugArchiveKind.End => "end",
|
|
_ => "continue"
|
|
};
|
|
}
|
|
|
|
private static string GetLocalTimezone()
|
|
{
|
|
try
|
|
{
|
|
var offset = DateTimeOffset.Now.Offset;
|
|
var sign = offset < TimeSpan.Zero ? "-" : "+";
|
|
return $"{TimeZoneInfo.Local.Id} (UTC{sign}{offset.Duration():hh\\:mm})";
|
|
}
|
|
catch
|
|
{
|
|
return DateTimeOffset.Now.Offset.ToString();
|
|
}
|
|
}
|
|
}
|
|
}
|