diff --git a/Infrastructure/ModuleDependencyService.cs b/Infrastructure/ModuleDependencyService.cs index 923b211b..436c4d40 100644 --- a/Infrastructure/ModuleDependencyService.cs +++ b/Infrastructure/ModuleDependencyService.cs @@ -20,6 +20,12 @@ internal sealed class ModuleDependencyService ClassStateCatalog.CategoryBoss5 ]; + // “锚点”是配置协议使用但不在可编辑字段目录中展示的固定字段。 + private static readonly HashSet HiddenFixedStateFields = new(StringComparer.Ordinal) + { + "锚点" + }; + private readonly string _classDirectory; private readonly string _classMacrosPath; private readonly object _gate = new(); @@ -119,6 +125,15 @@ internal sealed class ModuleDependencyService try { + ValidateSnapshot(module, module.Dependencies); + var removedFields = RemoveUnknownStateFields(module.Dependencies.Config.Spec); + if (removedFields.Count > 0) + { + result.SanitizedModules.Add(module.Clone()); + result.RemovedStateFields.AddRange(removedFields.Select(field => + new RemovedModuleStateField(module.Name, field.Category, field.Name))); + } + ImportOne(module, result); } catch (Exception ex) @@ -133,7 +148,6 @@ internal sealed class ModuleDependencyService private void ImportOne(ModuleDefinition module, ModuleDependencyImportResult result) { var snapshot = module.Dependencies!; - ValidateSnapshot(module, snapshot); var classPath = ResolveClassPath(snapshot.ClassId); var configDocument = ClassBlocksStore.Load(classPath); @@ -203,6 +217,62 @@ internal sealed class ModuleDependencyService } } + private static List RemoveUnknownStateFields(ModuleSpecSnapshot spec) + { + var removed = new List(); + if (spec.NestedStates) + { + foreach (var (category, fields) in spec.CategorizedStates) + { + if (fields is null) + { + continue; + } + + for (var index = fields.Count - 1; index >= 0; index--) + { + var field = fields[index]?.Trim() ?? string.Empty; + if (IsRecognizedStateField(category, field)) + { + continue; + } + + fields.RemoveAt(index); + if (field.Length > 0) + { + removed.Add(new RemovedStateField(category, field)); + } + } + } + } + else + { + for (var index = spec.FlatStates.Count - 1; index >= 0; index--) + { + var field = spec.FlatStates[index]?.Trim() ?? string.Empty; + if (HiddenFixedStateFields.Contains(field) || ClassStateCatalog.FindCategory(field) is not null) + { + continue; + } + + spec.FlatStates.RemoveAt(index); + if (field.Length > 0) + { + removed.Add(new RemovedStateField(ClassStateCatalog.CategoryState, field)); + } + } + } + + removed.Reverse(); + return removed; + } + + private static bool IsRecognizedStateField(string category, string field) + => field.Length > 0 + && (ClassStateCatalog.IsKnown(category, field) + || string.Equals(category, ClassStateCatalog.CategoryState, StringComparison.Ordinal) + && HiddenFixedStateFields.Contains(field)); + private string ResolveClassPath(int classId) { var path = Path.Combine(_classDirectory, ClassNames.GetConfigFileName(classId) + ".lua"); @@ -1059,7 +1129,11 @@ internal sealed class ModuleDependencyImportResult public List Conflicts { get; } = new(); public HashSet ConflictedModuleIds { get; } = new(StringComparer.OrdinalIgnoreCase); public List Rejected { get; } = new(); + public List SanitizedModules { get; } = new(); + public List RemovedStateFields { get; } = new(); public bool HasChanges => ConfigAdded > 0 || ConfigUpdated > 0 || MacrosAdded > 0; } internal sealed record RejectedModuleDependency(string ModuleId, string ModuleName, string Reason); +internal sealed record RemovedModuleStateField(string ModuleName, string Category, string Name); +internal sealed record RemovedStateField(string Category, string Name); diff --git a/Modules/ModuleStore.cs b/Modules/ModuleStore.cs index a53876c8..eb2dcb07 100644 --- a/Modules/ModuleStore.cs +++ b/Modules/ModuleStore.cs @@ -475,6 +475,36 @@ public sealed class ModuleStore } } + /// + /// 回写导入阶段清理过的依赖快照,并保留模块原有文件位置。 + /// + public ModuleDefinition SaveDependencyCleanup(ModuleDefinition module) + { + Normalize(module); + lock (_gate) + { + var existing = _modules.FirstOrDefault(item => + string.Equals(item.Id, module.Id, StringComparison.OrdinalIgnoreCase)); + if (existing is null || string.IsNullOrWhiteSpace(existing.FilePath)) + { + throw new InvalidOperationException($"找不到要清理的模块“{module.Name}”。"); + } + + var path = existing.FilePath; + if (!IsInsideModuleDirectory(path) || !File.Exists(path)) + { + throw new InvalidOperationException($"模块文件不在模块目录中或已不存在: {path}"); + } + + module.FilePath = path; + WriteFileAtomically(path, JsonSerializer.Serialize(module, JsonOptions)); + _modules.Remove(existing); + _modules.Add(module.Clone()); + _modules = SortModules(_modules).ToList(); + return module.Clone(); + } + } + public void Delete(ModuleDefinition module) { lock (_gate) diff --git a/UI/MainForm.cs b/UI/MainForm.cs index 3935fd0a..c4826239 100644 --- a/UI/MainForm.cs +++ b/UI/MainForm.cs @@ -304,6 +304,21 @@ public sealed class MainForm : Form, IMessageFilter return false; } + var cleanedModuleCount = 0; + var cleanupErrors = new List(); + foreach (var module in result.SanitizedModules) + { + try + { + _moduleStore.SaveDependencyCleanup(module); + cleanedModuleCount++; + } + catch (Exception ex) + { + cleanupErrors.Add($"{module.Name}: {ex.Message}"); + } + } + _moduleStore.SetImportIssues( result.Rejected.Select(item => item.ModuleId), result.ConflictedModuleIds); @@ -318,6 +333,18 @@ public sealed class MainForm : Form, IMessageFilter { AppendLog($"模块依赖冲突: {conflict}"); } + foreach (var field in result.RemovedStateFields.Take(50)) + { + AppendLog($"模块“{field.ModuleName}”已忽略未识别配置字段: {field.Category}.{field.Name}"); + } + if (result.RemovedStateFields.Count > 0) + { + AppendLog($"已从 {cleanedModuleCount} 个模块删除 {result.RemovedStateFields.Count} 个未识别配置字段,未导入本地配置。"); + } + foreach (var error in cleanupErrors) + { + AppendLog($"模块依赖字段清理回写失败: {error}"); + } string? postUpdateError = null; if (result.HasChanges) @@ -337,7 +364,10 @@ public sealed class MainForm : Form, IMessageFilter } } - if (showFeedback && (result.HasChanges || result.Rejected.Count > 0 || result.Conflicts.Count > 0)) + if (showFeedback && (result.HasChanges + || result.Rejected.Count > 0 + || result.Conflicts.Count > 0 + || result.RemovedStateFields.Count > 0)) { var lines = new List(); if (result.HasChanges) @@ -353,11 +383,19 @@ public sealed class MainForm : Form, IMessageFilter { lines.Add($"发现 {result.Conflicts.Count} 项冲突,均已保留本地内容;详情见日志。"); } + if (result.RemovedStateFields.Count > 0) + { + lines.Add($"已忽略 {result.RemovedStateFields.Count} 个未识别配置字段,并从 {cleanedModuleCount} 个模块中删除;这些字段未写入本地配置。"); + } + if (cleanupErrors.Count > 0) + { + lines.Add($"有 {cleanupErrors.Count} 个模块无法回写清理结果;本次仍未导入这些未识别字段,详情见日志。"); + } if (!string.IsNullOrWhiteSpace(postUpdateError)) { lines.Add($"本地依赖已写入,但 config/keymap 或游戏同步更新失败:{postUpdateError}"); } - var hasWarning = result.Rejected.Count > 0 || postUpdateError is not null; + var hasWarning = result.Rejected.Count > 0 || cleanupErrors.Count > 0 || postUpdateError is not null; MessageBox.Show( string.Join(Environment.NewLine, lines), hasWarning ? "模块导入完成(有警告)" : "模块导入完成",