Files
2026-08-17 18:18:13 +08:00

170 lines
8.2 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace ReplacedPerson.Core
{
public interface IReplacedPersonMod
{
string Id { get; }
string Version { get; }
void Register(IReplacedPersonRegistry registry);
}
public interface IReplacedPersonRegistry
{
void RegisterCard(ReplacedCardDefinition definition);
void RegisterEndCard(ReplacedEndCardDefinition definition);
void RegisterEnemy(ReplacedEnemyDefinition definition);
void RegisterReward(string id, string displayName);
void RegisterStory(string id, string title, string body);
void RegisterEffectResolver(string effectId, IReplacedPersonEffectResolver resolver);
void RegisterCommand(string path, Func<IReadOnlyList<string>, string> handler);
}
public interface IReplacedPersonEffectResolver
{
ReplacedEffectResult Resolve(ReplacedEffectContext context);
}
public sealed class ReplacedEffectContext
{
public string EffectId { get; set; } = string.Empty;
public int OwnerIndex { get; set; }
public int OpponentIndex => 1 - OwnerIndex;
public int Value { get; set; }
public ReplacedMatchState State { get; set; } = null!;
}
public sealed class ReplacedEffectResult
{
public int BonusDamage { get; set; }
public int CostDelta { get; set; }
public int DrawDelta { get; set; }
public string Log { get; set; } = string.Empty;
}
[Serializable]
public sealed class ReplacedPersonModFile
{
public string Path { get; set; } = string.Empty;
public string Sha256 { get; set; } = string.Empty;
}
[Serializable]
public sealed class ReplacedPersonModManifest
{
public string Id { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public List<ReplacedPersonModFile> Files { get; set; } = new();
}
public sealed class ReplacedModCompatibilityResult
{
public bool Compatible => Differences.Count == 0;
public List<string> Differences { get; } = new();
}
public static class ReplacedModCompatibility
{
public static ReplacedModCompatibilityResult Compare(IEnumerable<ReplacedPersonModManifest> authority,
IEnumerable<ReplacedPersonModManifest> client)
{
var result = new ReplacedModCompatibilityResult();
var expected = authority.ToDictionary(value => value.Id, StringComparer.Ordinal);
var actual = client.ToDictionary(value => value.Id, StringComparer.Ordinal);
foreach (var id in expected.Keys.Union(actual.Keys, StringComparer.Ordinal).OrderBy(value => value, StringComparer.Ordinal))
{
if (!expected.TryGetValue(id, out var expectedMod)) { result.Differences.Add("unexpected-mod:" + id); continue; }
if (!actual.TryGetValue(id, out var actualMod)) { result.Differences.Add("missing-mod:" + id); continue; }
if (!string.Equals(expectedMod.Version, actualMod.Version, StringComparison.Ordinal))
result.Differences.Add($"version:{id}:{expectedMod.Version}!={actualMod.Version}");
CompareFiles(id, expectedMod.Files, actualMod.Files, result.Differences);
}
return result;
}
public static string ComputeManifestSetHash(IEnumerable<ReplacedPersonModManifest> manifests)
{
var text = new StringBuilder();
foreach (var manifest in manifests.OrderBy(value => value.Id, StringComparer.Ordinal))
{
text.Append(manifest.Id).Append('|').Append(manifest.Version).Append('\n');
foreach (var file in manifest.Files.OrderBy(value => value.Path, StringComparer.Ordinal))
text.Append(file.Path).Append('|').Append(file.Sha256.ToLowerInvariant()).Append('\n');
}
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(text.ToString()));
return string.Concat(bytes.Select(value => value.ToString("x2")));
}
private static void CompareFiles(string modId, IEnumerable<ReplacedPersonModFile> expected,
IEnumerable<ReplacedPersonModFile> actual, ICollection<string> differences)
{
var expectedFiles = expected.ToDictionary(value => value.Path, StringComparer.Ordinal);
var actualFiles = actual.ToDictionary(value => value.Path, StringComparer.Ordinal);
foreach (var path in expectedFiles.Keys.Union(actualFiles.Keys, StringComparer.Ordinal).OrderBy(value => value, StringComparer.Ordinal))
{
if (!expectedFiles.TryGetValue(path, out var expectedFile)) { differences.Add($"unexpected-file:{modId}:{path}"); continue; }
if (!actualFiles.TryGetValue(path, out var actualFile)) { differences.Add($"missing-file:{modId}:{path}"); continue; }
if (!string.Equals(expectedFile.Sha256, actualFile.Sha256, StringComparison.OrdinalIgnoreCase))
differences.Add($"hash:{modId}:{path}");
}
}
}
public sealed class ReplacedPersonRegistry : IReplacedPersonRegistry
{
private readonly ReplacedContentCatalog _catalog;
private readonly Dictionary<string, string> _rewards = new(StringComparer.Ordinal);
private readonly Dictionary<string, (string title, string body)> _stories = new(StringComparer.Ordinal);
private readonly Dictionary<string, IReplacedPersonEffectResolver> _effects = new(StringComparer.Ordinal);
private readonly Dictionary<string, Func<IReadOnlyList<string>, string>> _commands = new(StringComparer.OrdinalIgnoreCase);
public ReplacedPersonRegistry(ReplacedContentCatalog baseCatalog) => _catalog = baseCatalog.Clone();
public ReplacedContentCatalog CreateSnapshot() => _catalog.Clone();
public IReadOnlyDictionary<string, IReplacedPersonEffectResolver> Effects => _effects;
public IReadOnlyDictionary<string, Func<IReadOnlyList<string>, string>> Commands => _commands;
public void RegisterCard(ReplacedCardDefinition definition) => AddUnique(_catalog.Cards, definition.Id, definition);
public void RegisterEndCard(ReplacedEndCardDefinition definition) => AddUnique(_catalog.EndCards, definition.Id, definition);
public void RegisterEnemy(ReplacedEnemyDefinition definition) => AddUnique(_catalog.Enemies, definition.Id, definition);
public void RegisterReward(string id, string displayName) => AddUnique(_rewards, id, displayName);
public void RegisterStory(string id, string title, string body) => AddUnique(_stories, id, (title, body));
public void RegisterEffectResolver(string effectId, IReplacedPersonEffectResolver resolver)
{
AddUnique(_effects, effectId, resolver);
AddUnique(_catalog.EffectResolvers, effectId, resolver);
}
public void RegisterCommand(string path, Func<IReadOnlyList<string>, string> handler) => AddUnique(_commands, path, handler);
private static void AddUnique<T>(IDictionary<string, T> target, string id, T value)
{
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Registry id must not be empty.", nameof(id));
var key = id.Trim();
if (target.ContainsKey(key)) throw new InvalidOperationException("Duplicate registry id: " + id);
target.Add(key, value);
}
}
public sealed class ReplacedHotReloadCatalog
{
public ReplacedContentCatalog Current { get; private set; }
public ReplacedContentCatalog? Pending { get; private set; }
public string CurrentHash => Current.ComputeHash();
public string PendingHash => Pending?.ComputeHash() ?? string.Empty;
public ReplacedHotReloadCatalog(ReplacedContentCatalog initial) => Current = initial.Clone();
public void Stage(ReplacedContentCatalog next) => Pending = next.Clone();
public ReplacedContentCatalog SnapshotForNewMatch()
{
if (Pending != null) { Current = Pending; Pending = null; }
return Current.Clone();
}
}
}