Files
Shigure/Infrastructure/FuyutsuiAddonSyncService.cs
waynebian01 6d1829f734 Add texture layout documentation for Fuyutsui in Chinese
- Introduced a new markdown file detailing the texture sorting mechanism for ClassBlocks.
- Explained the allocation of states, auras, spells, and group pixels, along with the independent layout of auxiliary bars.
- Provided a comprehensive overview of the output areas, sorting order, and pixel encoding.
- Included examples and guidelines for external reading and integration with existing systems.
2026-08-10 13:05:18 +08:00

147 lines
5.1 KiB
C#

using System.Security.Cryptography;
namespace Shigure;
/// <summary>
/// 将程序目录中的 Fuyutsui 单向部署到当前目标游戏。项目目录始终是权威源。
/// </summary>
internal sealed class FuyutsuiAddonSyncService
{
private const string AddonDirectoryName = "Fuyutsui";
private readonly string _sourceRoot;
private readonly WowProcessLocator _processLocator;
public FuyutsuiAddonSyncService(string sourceRoot, WowProcessLocator processLocator)
{
_sourceRoot = Path.GetFullPath(sourceRoot);
_processLocator = processLocator;
}
public string SourceRoot => _sourceRoot;
public FuyutsuiAddonSyncResult SynchronizeAll()
{
if (!Directory.Exists(_sourceRoot))
{
throw new DirectoryNotFoundException($"找不到项目 Fuyutsui 目录: {_sourceRoot}");
}
var targetRoot = ResolveTargetRoot();
if (targetRoot is null)
{
return FuyutsuiAddonSyncResult.TargetNotFound(_sourceRoot);
}
var copied = new List<string>();
var skipped = new List<string>();
var failures = new List<FuyutsuiAddonSyncFailure>();
foreach (var sourcePath in Directory.EnumerateFiles(_sourceRoot, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(_sourceRoot, sourcePath);
SynchronizeCore(sourcePath, relativePath, targetRoot, copied, skipped, failures);
}
return new FuyutsuiAddonSyncResult(_sourceRoot, targetRoot, copied, skipped, failures, null);
}
public FuyutsuiAddonSyncResult SynchronizeFile(string sourcePath)
{
var fullSourcePath = Path.GetFullPath(sourcePath);
var relativePath = Path.GetRelativePath(_sourceRoot, fullSourcePath);
if (Path.IsPathRooted(relativePath)
|| relativePath.Equals("..", StringComparison.Ordinal)
|| relativePath.StartsWith($"..{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
{
throw new InvalidOperationException($"待同步文件不在项目 Fuyutsui 目录内: {fullSourcePath}");
}
if (!File.Exists(fullSourcePath))
{
throw new FileNotFoundException("找不到待同步的项目插件文件。", fullSourcePath);
}
var targetRoot = ResolveTargetRoot();
if (targetRoot is null)
{
return FuyutsuiAddonSyncResult.TargetNotFound(_sourceRoot);
}
var copied = new List<string>();
var skipped = new List<string>();
var failures = new List<FuyutsuiAddonSyncFailure>();
SynchronizeCore(fullSourcePath, relativePath, targetRoot, copied, skipped, failures);
return new FuyutsuiAddonSyncResult(_sourceRoot, targetRoot, copied, skipped, failures, null);
}
private string? ResolveTargetRoot()
{
var addOnsDirectory = WowAddonLocator.FindAddOnsDirectory(_processLocator);
return string.IsNullOrWhiteSpace(addOnsDirectory)
? null
: Path.Combine(addOnsDirectory, AddonDirectoryName);
}
private static void SynchronizeCore(
string sourcePath,
string relativePath,
string targetRoot,
ICollection<string> copied,
ICollection<string> skipped,
ICollection<FuyutsuiAddonSyncFailure> failures)
{
try
{
var targetPath = Path.Combine(targetRoot, relativePath);
if (File.Exists(targetPath) && FilesHaveSameHash(sourcePath, targetPath))
{
skipped.Add(relativePath);
return;
}
var targetDirectory = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrWhiteSpace(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
File.Copy(sourcePath, targetPath, overwrite: true);
copied.Add(relativePath);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or CryptographicException)
{
failures.Add(new FuyutsuiAddonSyncFailure(relativePath, ex.Message));
}
}
private static bool FilesHaveSameHash(string firstPath, string secondPath)
{
using var first = File.OpenRead(firstPath);
using var second = File.OpenRead(secondPath);
var firstHash = SHA256.HashData(first);
var secondHash = SHA256.HashData(second);
return firstHash.AsSpan().SequenceEqual(secondHash);
}
}
internal sealed record FuyutsuiAddonSyncFailure(string RelativePath, string Message);
internal sealed record FuyutsuiAddonSyncResult(
string SourceRoot,
string? TargetRoot,
IReadOnlyList<string> CopiedFiles,
IReadOnlyList<string> SkippedFiles,
IReadOnlyList<FuyutsuiAddonSyncFailure> Failures,
string? SkippedReason)
{
public bool TargetFound => TargetRoot is not null;
public bool CompletedSuccessfully => TargetFound && Failures.Count == 0;
public static FuyutsuiAddonSyncResult TargetNotFound(string sourceRoot) => new(
sourceRoot,
null,
[],
[],
[],
"未找到目标游戏进程,已跳过游戏插件同步。");
}