按 spellId 合并模块配置

This commit is contained in:
waynebian01
2026-08-23 21:43:13 +08:00
parent 139c6451f7
commit 8e6f8904f6
2 changed files with 359 additions and 27 deletions

View File

@@ -154,14 +154,15 @@ internal sealed class ModuleDependencyService
MergeMacros(localMacros, snapshot.SpecId, snapshot.Macros, counters);
EnsureMacroCapacity(snapshot.ClassId, localMacros);
if (counters.ConfigAdded == 0 && counters.MacrosAdded == 0)
if (!counters.HasConfigChanges && counters.MacrosAdded == 0)
{
result.Conflicts.AddRange(counters.Conflicts.Select(message => $"{module.Name}: {message}"));
return;
}
CommitDocuments(configDocument, macrosDocument, counters.ConfigAdded > 0, counters.MacrosAdded > 0);
CommitDocuments(configDocument, macrosDocument, counters.HasConfigChanges, counters.MacrosAdded > 0);
result.ConfigAdded += counters.ConfigAdded;
result.ConfigUpdated += counters.ConfigUpdated;
result.MacrosAdded += counters.MacrosAdded;
result.ChangedModules.Add(module.Name);
result.Conflicts.AddRange(counters.Conflicts.Select(message => $"{module.Name}: {message}"));
@@ -318,15 +319,14 @@ internal sealed class ModuleDependencyService
string label,
MergeCounters counters)
{
CompactLocalAuras(local, label, counters);
foreach (var entry in incoming)
{
var existing = local.FirstOrDefault(item => string.Equals(item.Name, entry.Name, StringComparison.Ordinal));
var existing = local.FirstOrDefault(item => AuraIdentityMatches(item, entry));
if (existing is not null)
{
if (!AuraEquals(existing, entry))
{
counters.Conflicts.Add($"{label}“{entry.Name}”与本地内容不同,已保留本地。");
}
MergeAuraMetadata(existing, entry, label, counters);
continue;
}
@@ -342,16 +342,148 @@ internal sealed class ModuleDependencyService
}
}
private static void CompactLocalAuras(
List<ClassBlocksStore.AuraEntry> local,
string label,
MergeCounters counters)
{
for (var index = 0; index < local.Count; index++)
{
var current = local[index];
var existing = local.Take(index).FirstOrDefault(item => AuraIdentityMatches(item, current));
if (existing is null)
{
continue;
}
MergeAuraMetadata(existing, current, label, counters);
local.RemoveAt(index--);
counters.ConfigUpdated++;
}
}
private static bool AuraIdentityMatches(
ClassBlocksStore.AuraEntry left,
ClassBlocksStore.AuraEntry right)
{
return SpellIdentityMatches(
GetAuraSpellIds(left.SpellId, left.SpellIds),
left.Name,
GetAuraSpellIds(right.SpellId, right.SpellIds),
right.Name);
}
private static bool AuraIdentityMatches(
ClassBlocksStore.AuraEntry left,
ModuleAuraSnapshot right)
{
return SpellIdentityMatches(
GetAuraSpellIds(left.SpellId, left.SpellIds),
left.Name,
GetAuraSpellIds(right.SpellId, right.SpellIds),
right.Name);
}
private static void MergeAuraMetadata(
ClassBlocksStore.AuraEntry target,
ClassBlocksStore.AuraEntry incoming,
string label,
MergeCounters counters)
{
MergeAuraMetadataCore(
target,
incoming.Name,
incoming.SpellId,
incoming.SpellIds,
incoming.MaxApps,
label,
counters);
}
private static void MergeAuraMetadata(
ClassBlocksStore.AuraEntry target,
ModuleAuraSnapshot incoming,
string label,
MergeCounters counters)
{
MergeAuraMetadataCore(
target,
incoming.Name,
incoming.SpellId,
incoming.SpellIds,
incoming.MaxApps,
label,
counters);
}
private static void MergeAuraMetadataCore(
ClassBlocksStore.AuraEntry target,
string? incomingName,
long? incomingSpellId,
IEnumerable<long>? incomingSpellIds,
int? incomingMaxApps,
string label,
MergeCounters counters)
{
var changed = false;
if (target.SpellId is null && incomingSpellId is > 0)
{
target.SpellId = incomingSpellId;
changed = true;
}
foreach (var spellId in GetAuraSpellIds(incomingSpellId, incomingSpellIds))
{
if (target.SpellId != spellId && !target.SpellIds.Contains(spellId))
{
target.SpellIds.Add(spellId);
changed = true;
}
}
if (target.MaxApps is null && incomingMaxApps is not null)
{
target.MaxApps = incomingMaxApps;
changed = true;
}
else if (target.MaxApps is not null
&& incomingMaxApps is not null
&& target.MaxApps != incomingMaxApps)
{
var mergedMaxApps = Math.Max(target.MaxApps.Value, incomingMaxApps.Value);
counters.Conflicts.Add(
$"{label}“{DisplayName(incomingName, incomingSpellId)}”的 maxApps 存在差异:"
+ $"{target.MaxApps}、{incomingMaxApps},已采用 {mergedMaxApps}。");
if (target.MaxApps != mergedMaxApps)
{
target.MaxApps = mergedMaxApps;
changed = true;
}
}
if (string.IsNullOrWhiteSpace(target.Name) && !string.IsNullOrWhiteSpace(incomingName))
{
target.Name = incomingName;
changed = true;
}
if (changed)
{
counters.ConfigUpdated++;
}
}
private static void MergeSpells(
List<ClassBlocksStore.SpellEntry> local,
IEnumerable<ModuleSpellSnapshot> incoming,
MergeCounters counters)
{
CompactLocalSpells(local, counters);
foreach (var entry in incoming)
{
var name = string.IsNullOrWhiteSpace(entry.Name) ? entry.SpellId.ToString() : entry.Name;
var existing = local.FirstOrDefault(item =>
string.Equals(string.IsNullOrWhiteSpace(item.Name) ? item.SpellId.ToString() : item.Name, name, StringComparison.Ordinal));
var name = DisplayName(entry.Name, entry.SpellId);
var existing = local.FirstOrDefault(item => SpellIdentityMatches(item, entry));
if (existing is not null)
{
if (!SpellEquals(existing, entry))
@@ -375,11 +507,98 @@ internal sealed class ModuleDependencyService
}
}
private static void CompactLocalSpells(
List<ClassBlocksStore.SpellEntry> local,
MergeCounters counters)
{
for (var index = 0; index < local.Count; index++)
{
var current = local[index];
var existing = local.Take(index).FirstOrDefault(item => SpellIdentityMatches(item, current));
if (existing is null)
{
continue;
}
local.RemoveAt(index--);
counters.ConfigUpdated++;
}
}
private static bool SpellIdentityMatches(
ClassBlocksStore.SpellEntry left,
ClassBlocksStore.SpellEntry right)
{
return SpellIdentityMatches(
GetSpellIds(left.SpellId),
left.Name,
GetSpellIds(right.SpellId),
right.Name);
}
private static bool SpellIdentityMatches(
ClassBlocksStore.SpellEntry left,
ModuleSpellSnapshot right)
{
return SpellIdentityMatches(
GetSpellIds(left.SpellId),
left.Name,
GetSpellIds(right.SpellId),
right.Name);
}
private static bool SpellIdentityMatches(
IEnumerable<long> leftIds,
string? leftName,
IEnumerable<long> rightIds,
string? rightName)
{
var leftIdSet = leftIds.Where(IsValidSpellId).ToHashSet();
var rightIdSet = rightIds.Where(IsValidSpellId).ToHashSet();
if (leftIdSet.Count > 0 || rightIdSet.Count > 0)
{
return leftIdSet.Count > 0
&& rightIdSet.Count > 0
&& leftIdSet.Overlaps(rightIdSet);
}
return string.Equals(leftName?.Trim(), rightName?.Trim(), StringComparison.Ordinal);
}
private static IEnumerable<long> GetSpellIds(long spellId)
=> IsValidSpellId(spellId) ? [spellId] : [];
private static IEnumerable<long> GetAuraSpellIds(long? spellId, IEnumerable<long>? spellIds)
{
var result = new HashSet<long>();
if (spellId is { } primary && IsValidSpellId(primary))
{
result.Add(primary);
}
foreach (var id in spellIds ?? [])
{
if (IsValidSpellId(id))
{
result.Add(id);
}
}
return result;
}
private static bool IsValidSpellId(long spellId) => spellId > 0;
private static string DisplayName(string? name, long? spellId)
=> string.IsNullOrWhiteSpace(name) ? spellId?.ToString() ?? "未命名" : name.Trim();
private static void MergeSpellsList(
List<ClassBlocksStore.SpellsListEntry> local,
IEnumerable<ModuleSpellListEntrySnapshot> incoming,
MergeCounters counters)
{
CompactLocalSpellsList(local, counters);
foreach (var entry in incoming)
{
var byId = local.FirstOrDefault(item => item.SpellId == entry.SpellId);
@@ -407,6 +626,23 @@ internal sealed class ModuleDependencyService
}
}
private static void CompactLocalSpellsList(
List<ClassBlocksStore.SpellsListEntry> local,
MergeCounters counters)
{
var seen = new HashSet<long>();
for (var index = 0; index < local.Count; index++)
{
if (seen.Add(local[index].SpellId))
{
continue;
}
local.RemoveAt(index--);
counters.ConfigUpdated++;
}
}
private static void MergeGroup(
ClassBlocksStore.SpecBlocks localSpec,
ModuleGroupSnapshot? incoming,
@@ -426,16 +662,25 @@ internal sealed class ModuleDependencyService
Role = incoming.Role,
Dispel = incoming.Dispel
};
counters.ConfigAdded++;
foreach (var aura in incoming.Auras)
{
var existing = group.Auras.FirstOrDefault(item => GroupAuraIdentityMatches(item, aura));
if (existing is not null)
{
MergeGroupAuraMetadata(existing, aura, counters);
continue;
}
group.Auras.Add(ToGroupAura(aura, aura.Offset));
counters.ConfigAdded++;
}
localSpec.Group = group;
counters.ConfigAdded += 1 + incoming.Auras.Count;
return;
}
var local = localSpec.Group;
CompactLocalGroupAuras(local.Auras, counters);
var occupied = new HashSet<int>(local.Auras.Select(aura => aura.Offset));
AddOffset(local.HealthPercent);
AddOffset(local.Role);
@@ -459,13 +704,10 @@ internal sealed class ModuleDependencyService
foreach (var aura in incoming.Auras)
{
var existing = local.Auras.FirstOrDefault(item => string.Equals(item.Name, aura.Name, StringComparison.Ordinal));
var existing = local.Auras.FirstOrDefault(item => GroupAuraIdentityMatches(item, aura));
if (existing is not null)
{
if (!GroupAuraEquals(existing, aura))
{
counters.Conflicts.Add($"队伍光环“{aura.Name}”与本地内容不同,已保留本地。");
}
MergeGroupAuraMetadata(existing, aura, counters);
continue;
}
@@ -488,6 +730,100 @@ internal sealed class ModuleDependencyService
}
}
private static bool GroupAuraIdentityMatches(
ClassBlocksStore.GroupAuraEntry left,
ModuleGroupAuraSnapshot right)
{
// offset 是队伍光环的槽位,同一 spellId 出现在不同槽位时仍然是两个槽位。
return left.Offset == right.Offset
&& SpellIdentityMatches(
GetAuraSpellIds(left.SpellId, left.SpellIds),
left.Name,
GetAuraSpellIds(right.SpellId, right.SpellIds),
right.Name);
}
private static bool GroupAuraIdentityMatches(
ClassBlocksStore.GroupAuraEntry left,
ClassBlocksStore.GroupAuraEntry right)
{
return left.Offset == right.Offset
&& SpellIdentityMatches(
GetAuraSpellIds(left.SpellId, left.SpellIds),
left.Name,
GetAuraSpellIds(right.SpellId, right.SpellIds),
right.Name);
}
private static void CompactLocalGroupAuras(
List<ClassBlocksStore.GroupAuraEntry> local,
MergeCounters counters)
{
for (var index = 0; index < local.Count; index++)
{
var current = local[index];
var existing = local.Take(index).FirstOrDefault(item => GroupAuraIdentityMatches(item, current));
if (existing is null)
{
continue;
}
if (existing.SpellId is null && current.SpellId is > 0)
{
existing.SpellId = current.SpellId;
}
foreach (var spellId in GetAuraSpellIds(null, current.SpellIds))
{
if (!existing.SpellIds.Contains(spellId))
{
existing.SpellIds.Add(spellId);
}
}
if (string.IsNullOrWhiteSpace(existing.Name) && !string.IsNullOrWhiteSpace(current.Name))
{
existing.Name = current.Name;
}
local.RemoveAt(index--);
counters.ConfigUpdated++;
}
}
private static void MergeGroupAuraMetadata(
ClassBlocksStore.GroupAuraEntry target,
ModuleGroupAuraSnapshot incoming,
MergeCounters counters)
{
var changed = false;
if (target.SpellId is null && incoming.SpellId is > 0)
{
target.SpellId = incoming.SpellId;
changed = true;
}
foreach (var spellId in GetAuraSpellIds(null, incoming.SpellIds))
{
if (!target.SpellIds.Contains(spellId))
{
target.SpellIds.Add(spellId);
changed = true;
}
}
if (string.IsNullOrWhiteSpace(target.Name) && !string.IsNullOrWhiteSpace(incoming.Name))
{
target.Name = incoming.Name;
changed = true;
}
if (changed)
{
counters.ConfigUpdated++;
}
}
private static int AllocateOffset(int desired, ISet<int> occupied)
{
var offset = desired > 0 && !occupied.Contains(desired) ? desired : 1;
@@ -672,11 +1008,6 @@ internal sealed class ModuleDependencyService
}
}
private static bool AuraEquals(ClassBlocksStore.AuraEntry left, ModuleAuraSnapshot right)
=> left.SpellId == right.SpellId
&& left.MaxApps == right.MaxApps
&& left.SpellIds.SequenceEqual(right.SpellIds);
private static bool SpellEquals(ClassBlocksStore.SpellEntry left, ModuleSpellSnapshot right)
=> left.SpellId == right.SpellId
&& left.Charge == right.Charge
@@ -685,9 +1016,6 @@ internal sealed class ModuleDependencyService
&& left.ForcedKnown == right.ForcedKnown
&& left.InSpellBook == right.InSpellBook;
private static bool GroupAuraEquals(ClassBlocksStore.GroupAuraEntry left, ModuleGroupAuraSnapshot right)
=> left.SpellId == right.SpellId && left.SpellIds.SequenceEqual(right.SpellIds);
private static bool MacroEntryEquals(ModuleMacroEntrySnapshot left, ModuleMacroEntrySnapshot right)
=> string.Equals(NormalizeMacroText(left.Text), NormalizeMacroText(right.Text), StringComparison.Ordinal)
&& string.Equals(left.Comment?.Trim(), right.Comment?.Trim(), StringComparison.Ordinal);
@@ -695,8 +1023,11 @@ internal sealed class ModuleDependencyService
private sealed class MergeCounters
{
public int ConfigAdded { get; set; }
public int ConfigUpdated { get; set; }
public int MacrosAdded { get; set; }
public List<string> Conflicts { get; } = new();
public bool HasConfigChanges => ConfigAdded > 0 || ConfigUpdated > 0;
}
// 静态宏和特殊宏可以解析成相同的目标/技能/条件,但仍是两个独立槽位。
@@ -707,11 +1038,12 @@ internal sealed class ModuleDependencyService
internal sealed class ModuleDependencyImportResult
{
public int ConfigAdded { get; set; }
public int ConfigUpdated { get; set; }
public int MacrosAdded { get; set; }
public List<string> ChangedModules { get; } = new();
public List<string> Conflicts { get; } = new();
public List<RejectedModuleDependency> Rejected { get; } = new();
public bool HasChanges => ConfigAdded > 0 || MacrosAdded > 0;
public bool HasChanges => ConfigAdded > 0 || ConfigUpdated > 0 || MacrosAdded > 0;
}
internal sealed record RejectedModuleDependency(string ModuleId, string ModuleName, string Reason);