383 lines
14 KiB
C#
383 lines
14 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 TH1_Logic.Config;
|
|
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 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 steamId;
|
|
public string version;
|
|
public string unityVersion;
|
|
public string platform;
|
|
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;
|
|
}
|
|
|
|
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 = Resources.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"),
|
|
steamId = steamId ?? "",
|
|
version = string.IsNullOrWhiteSpace(version) ? GetCurrentVersion() : version.Trim(),
|
|
unityVersion = Application.unityVersion,
|
|
platform = Application.platform.ToString(),
|
|
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 files = GetArchiveFiles(isMulti);
|
|
if (files.Count == 0) return false;
|
|
|
|
var groups = new Dictionary<uint, ArchiveGroup>();
|
|
foreach (var file in files)
|
|
{
|
|
if (!groups.TryGetValue(file.MapId, out var group))
|
|
{
|
|
group = new ArchiveGroup { MapId = file.MapId, IsMulti = isMulti };
|
|
groups[file.MapId] = group;
|
|
}
|
|
|
|
group.Add(file);
|
|
}
|
|
|
|
session = groups.Values
|
|
.Select(group => group.TryBuildSession())
|
|
.Where(value => value != null)
|
|
.OrderByDescending(value => value.LatestWriteTimeUtc)
|
|
.FirstOrDefault();
|
|
return session != null;
|
|
}
|
|
|
|
private static List<ArchiveFile> GetArchiveFiles(bool isMulti)
|
|
{
|
|
var result = new List<ArchiveFile>();
|
|
var directory = ConfigDirectory;
|
|
if (!Directory.Exists(directory)) return result;
|
|
|
|
foreach (var pattern in new[] { "map_archive_*.dat", "map_archive_*.dat.bak" })
|
|
{
|
|
foreach (var path in Directory.GetFiles(directory, pattern))
|
|
{
|
|
if (TryParseArchiveFile(path, out var file) && file.IsMulti == isMulti)
|
|
result.Add(file);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static bool TryParseArchiveFile(string path, out ArchiveFile file)
|
|
{
|
|
file = null;
|
|
|
|
var fileName = Path.GetFileName(path);
|
|
if (string.IsNullOrEmpty(fileName)) return false;
|
|
|
|
if (fileName.EndsWith(".bak", StringComparison.OrdinalIgnoreCase))
|
|
fileName = fileName.Substring(0, fileName.Length - ".bak".Length);
|
|
if (!fileName.EndsWith(".dat", StringComparison.OrdinalIgnoreCase)) return false;
|
|
|
|
var stem = fileName.Substring(0, fileName.Length - ".dat".Length);
|
|
const string prefix = "map_archive_";
|
|
if (!stem.StartsWith(prefix, StringComparison.Ordinal)) return false;
|
|
|
|
var rest = stem.Substring(prefix.Length);
|
|
var kind = PlayerBugArchiveKind.Continue;
|
|
if (rest.StartsWith("begin_", StringComparison.Ordinal))
|
|
{
|
|
kind = PlayerBugArchiveKind.Begin;
|
|
rest = rest.Substring("begin_".Length);
|
|
}
|
|
else if (rest.StartsWith("continue_", StringComparison.Ordinal))
|
|
{
|
|
kind = PlayerBugArchiveKind.Continue;
|
|
rest = rest.Substring("continue_".Length);
|
|
}
|
|
else if (rest.StartsWith("end_", StringComparison.Ordinal))
|
|
{
|
|
kind = PlayerBugArchiveKind.End;
|
|
rest = rest.Substring("end_".Length);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var isMulti = false;
|
|
if (rest.StartsWith("multi_", StringComparison.Ordinal))
|
|
{
|
|
isMulti = true;
|
|
rest = rest.Substring("multi_".Length);
|
|
}
|
|
|
|
if (!uint.TryParse(rest, out var mapId)) return false;
|
|
|
|
file = new ArchiveFile
|
|
{
|
|
Path = path,
|
|
Kind = kind,
|
|
IsMulti = isMulti,
|
|
MapId = mapId,
|
|
LastWriteTimeUtc = File.GetLastWriteTimeUtc(path)
|
|
};
|
|
return true;
|
|
}
|
|
|
|
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 entryPath = $"saves/{session.ModeLabel}/{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,
|
|
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 class ArchiveFile
|
|
{
|
|
public string Path;
|
|
public PlayerBugArchiveKind Kind;
|
|
public bool IsMulti;
|
|
public uint MapId;
|
|
public DateTime LastWriteTimeUtc;
|
|
}
|
|
|
|
private class ArchiveGroup
|
|
{
|
|
public uint MapId;
|
|
public bool IsMulti;
|
|
private ArchiveFile _begin;
|
|
private ArchiveFile _continue;
|
|
private ArchiveFile _end;
|
|
|
|
public void Add(ArchiveFile file)
|
|
{
|
|
switch (file.Kind)
|
|
{
|
|
case PlayerBugArchiveKind.Begin:
|
|
if (_begin == null || file.LastWriteTimeUtc > _begin.LastWriteTimeUtc) _begin = file;
|
|
break;
|
|
case PlayerBugArchiveKind.End:
|
|
if (_end == null || file.LastWriteTimeUtc > _end.LastWriteTimeUtc) _end = file;
|
|
break;
|
|
default:
|
|
if (_continue == null || file.LastWriteTimeUtc > _continue.LastWriteTimeUtc) _continue = file;
|
|
break;
|
|
}
|
|
}
|
|
|
|
public PlayerBugReportArchiveSession TryBuildSession()
|
|
{
|
|
if (_begin == null) return null;
|
|
var companion = PickCompanion();
|
|
if (companion == null) return null;
|
|
|
|
return new PlayerBugReportArchiveSession
|
|
{
|
|
IsMulti = IsMulti,
|
|
MapId = MapId,
|
|
BeginPath = _begin.Path,
|
|
CompanionPath = companion.Path,
|
|
CompanionKind = companion.Kind,
|
|
LatestWriteTimeUtc = _begin.LastWriteTimeUtc > companion.LastWriteTimeUtc
|
|
? _begin.LastWriteTimeUtc
|
|
: companion.LastWriteTimeUtc
|
|
};
|
|
}
|
|
|
|
private ArchiveFile PickCompanion()
|
|
{
|
|
if (_end == null) return _continue;
|
|
if (_continue == null) return _end;
|
|
return _end.LastWriteTimeUtc >= _continue.LastWriteTimeUtc ? _end : _continue;
|
|
}
|
|
}
|
|
}
|
|
}
|