diff --git a/Modules/ModuleStore.cs b/Modules/ModuleStore.cs index 576d606b..ce6a5642 100644 --- a/Modules/ModuleStore.cs +++ b/Modules/ModuleStore.cs @@ -183,6 +183,11 @@ public sealed class ModuleRule public string Hotkey { get; set; } = string.Empty; public string Step { get; set; } = string.Empty; + // 子条件: 与主条件是「且」关系, 子条件之间是「或」关系。 + // 命中 = 主条件成立 && (无子条件 || 任一子条件成立)。用于表达求值器写不出的 主 && (A || B)。 + // 旧模块无此字段 → 反序列化为 null; 序列化时 null 会被 WhenWritingNull 忽略。 + public List? SubConditions { get; set; } + public ModuleRule Clone() { return new ModuleRule @@ -193,9 +198,24 @@ public sealed class ModuleRule UnitName = UnitName, Spell = Spell, Hotkey = Hotkey, - Step = Step + Step = Step, + SubConditions = SubConditions is null ? null : new List(SubConditions) }; } + + // 供求值日志与规则表显示复用的可读描述, 避免在 UI 里另写一份。 + public string DescribeCondition() + { + if (SubConditions is not { Count: > 0 }) + { + return Condition; + } + + var any = string.Join(" | ", SubConditions); + return string.IsNullOrWhiteSpace(Condition) + ? $"任一({any})" + : $"{Condition} 且任一({any})"; + } } public sealed class ModuleValueAdjustment @@ -478,6 +498,23 @@ public sealed class ModuleStore } module.Rules ??= new List(); + foreach (var rule in module.Rules) + { + if (rule.SubConditions is null) + { + continue; + } + + // 去空白、丢空项; 整组为空则回到 null, 保持文件干净且求值不见空子条件。 + rule.SubConditions = rule.SubConditions + .Select(sub => sub?.Trim() ?? string.Empty) + .Where(sub => sub.Length > 0) + .ToList(); + if (rule.SubConditions.Count == 0) + { + rule.SubConditions = null; + } + } } private bool IsInsideModuleDirectory(string path) @@ -552,10 +589,10 @@ public static class ModuleLogic foreach (var rule in module.Rules.Where(rule => rule.Enabled)) { - if (!ModuleConditionEvaluator.TryEvaluate(rule.Condition, state, out var conditionMatched, out var error, failedSpells)) + if (!ModuleConditionEvaluator.TryEvaluateRule(rule, state, out var conditionMatched, out var error, failedSpells)) { info["条件错误"] = error; - info["规则条件"] = rule.Condition; + info["规则条件"] = rule.DescribeCondition(); return new LogicDecision(null, $"{module.Name}: 条件错误", info, module.Name); } @@ -566,7 +603,7 @@ public static class ModuleLogic if (ModuleSpecialActions.IsPauseSpell(rule.Spell)) { - info["命中条件"] = string.IsNullOrWhiteSpace(rule.Condition) ? "始终" : rule.Condition; + info["命中条件"] = string.IsNullOrWhiteSpace(rule.DescribeCondition()) ? "始终" : rule.DescribeCondition(); info["动作技能"] = ModuleSpecialActions.PauseSpell; info["动作按键"] = "-"; info["动作单位"] = "-"; @@ -963,6 +1000,49 @@ public static class ModuleConditionEvaluator return true; } + // 整条规则的命中判定: 主条件成立 且 (无子条件 || 任一子条件成立)。 + // 子条件之间是「或」, 与主条件是「且」; 任一子条件求值出错都按错误返回。 + public static bool TryEvaluateRule( + ModuleRule rule, + GameState state, + out bool matched, + out string? error, + IReadOnlyDictionary? failedSpells = null) + { + if (!TryEvaluate(rule.Condition, state, out matched, out error, failedSpells)) + { + return false; + } + + if (!matched || rule.SubConditions is not { Count: > 0 }) + { + return true; + } + + foreach (var sub in rule.SubConditions) + { + if (string.IsNullOrWhiteSpace(sub)) + { + continue; + } + + if (!TryEvaluate(sub, state, out var subMatched, out error, failedSpells)) + { + matched = false; + return false; + } + + if (subMatched) + { + matched = true; + return true; + } + } + + matched = false; + return true; + } + public static bool TryResolveInt(GameState state, string fieldName, out int value) { if (TryResolveDouble(state, fieldName, out var number)) diff --git a/Shigure.Installer/InstallerForm.cs b/Shigure.Installer/InstallerForm.cs deleted file mode 100644 index cb094409..00000000 --- a/Shigure.Installer/InstallerForm.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Diagnostics; - -namespace Shigure.Installer; - -/// -/// 安装器主界面:收集程序集名称 / 输出路径 / 是否保留配置,触发隔离构建流程。 -/// 实际构建逻辑全部在 ,本类只负责交互与状态。 -/// -internal sealed class InstallerForm : Form -{ - private readonly TextBox _nameTextBox; - private readonly TextBox _outputTextBox; - private readonly Button _browseButton; - private readonly Button _buildButton; - private readonly TextBox _logTextBox; - - private bool _building; - - private static readonly Color Background = Color.FromArgb(30, 30, 30); - private static readonly Color Field = Color.FromArgb(45, 45, 45); - private static readonly Color Accent = Color.FromArgb(0, 120, 215); - - public InstallerForm() - { - Text = "Shigure 安装器"; - Size = new Size(620, 560); - MinimumSize = new Size(560, 480); - StartPosition = FormStartPosition.CenterScreen; - BackColor = Background; - ForeColor = Color.White; - Font = new Font("Microsoft YaHei UI", 9); - - var titleLabel = new Label - { - Text = "生成定制版 Shigure", - Location = new Point(20, 18), - Size = new Size(560, 30), - Font = new Font("Microsoft YaHei UI", 14, FontStyle.Bold), - }; - - var nameLabel = MakeLabel("程序名称(字母 / 数字 / 下划线,不能以数字开头):", new Point(20, 60), 560); - - _nameTextBox = new TextBox - { - Location = new Point(20, 88), - Size = new Size(560, 28), - Font = new Font("Consolas", 11), - BackColor = Field, - ForeColor = Color.White, - BorderStyle = BorderStyle.FixedSingle, - Text = "MyWowHelper", - }; - - var outputLabel = MakeLabel("输出路径(留空 = 桌面):", new Point(20, 128), 560); - - _outputTextBox = new TextBox - { - Location = new Point(20, 156), - Size = new Size(460, 28), - BackColor = Field, - ForeColor = Color.White, - BorderStyle = BorderStyle.FixedSingle, - PlaceholderText = "未选择(将使用桌面)", - }; - - _browseButton = MakeButton("浏览…", new Point(490, 155), new Size(90, 30), Field); - _browseButton.Click += (_, _) => BrowseForOutput(); - - _buildButton = MakeButton("开始构建", new Point(20, 196), new Size(560, 42), Accent); - _buildButton.Font = new Font("Microsoft YaHei UI", 11, FontStyle.Bold); - _buildButton.Click += async (_, _) => await BuildAsync(); - - var logLabel = MakeLabel("构建日志:", new Point(20, 252), 560); - - _logTextBox = new TextBox - { - Location = new Point(20, 278), - Size = new Size(560, 224), - Multiline = true, - ScrollBars = ScrollBars.Vertical, - ReadOnly = true, - WordWrap = false, - Font = new Font("Consolas", 9), - BackColor = Color.FromArgb(20, 20, 20), - ForeColor = Color.FromArgb(120, 220, 120), - BorderStyle = BorderStyle.FixedSingle, - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - }; - - // 让输入控件随窗口横向拉伸。 - _nameTextBox.Anchor = _outputTextBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - _buildButton.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - _browseButton.Anchor = AnchorStyles.Top | AnchorStyles.Right; - - Controls.AddRange(new Control[] - { - titleLabel, nameLabel, _nameTextBox, - outputLabel, _outputTextBox, _browseButton, - _buildButton, logLabel, _logTextBox, - }); - - AcceptButton = _buildButton; - } - - private void BrowseForOutput() - { - using var dialog = new FolderBrowserDialog - { - Description = "选择输出文件夹", - UseDescriptionForTitle = true, - ShowNewFolderButton = true, - }; - - if (dialog.ShowDialog(this) == DialogResult.OK) - _outputTextBox.Text = dialog.SelectedPath; - } - - private async Task BuildAsync() - { - var programName = _nameTextBox.Text.Trim(); - - if (!ProjectBuilder.IsValidProgramName(programName)) - { - MessageBox.Show(this, - "程序名称不合法:只能包含英文字母、数字、下划线,且不能以数字开头。", - "请检查名称", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - - var baseDir = _outputTextBox.Text.Trim(); - if (baseDir.Length == 0) - baseDir = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); - - if (!Directory.Exists(baseDir)) - { - MessageBox.Show(this, $"输出路径不存在:{baseDir}", "请检查路径", - MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - - var outputDir = Path.Combine(baseDir, programName); - - SetBuilding(true); - _logTextBox.Clear(); - - try - { - var builder = new ProjectBuilder(LogMessage); - await Task.Run(() => builder.BuildAndPackage(programName, outputDir)); - - LogMessage(string.Empty); - LogMessage($"✓ 构建完成:{outputDir}"); - - var open = MessageBox.Show(this, - $"构建成功!\n\n输出目录:{outputDir}\n\n是否打开输出文件夹?", - "完成", MessageBoxButtons.YesNo, MessageBoxIcon.Information); - - if (open == DialogResult.Yes && Directory.Exists(outputDir)) - Process.Start("explorer.exe", $"\"{outputDir}\""); - } - catch (Exception ex) - { - LogMessage(string.Empty); - LogMessage($"✗ 构建失败:{ex.Message}"); - MessageBox.Show(this, $"构建失败:\n\n{ex.Message}", "错误", - MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally - { - SetBuilding(false); - } - } - - private void SetBuilding(bool building) - { - _building = building; - _buildButton.Text = building ? "构建中…" : "开始构建"; - _buildButton.Enabled = !building; - _nameTextBox.Enabled = !building; - _outputTextBox.Enabled = !building; - _browseButton.Enabled = !building; - UseWaitCursor = building; - } - - protected override void OnFormClosing(FormClosingEventArgs e) - { - if (_building) - { - MessageBox.Show(this, "正在构建中,请等待构建完成后再关闭。", "请稍候", - MessageBoxButtons.OK, MessageBoxIcon.Information); - e.Cancel = true; - return; - } - - base.OnFormClosing(e); - } - - private void LogMessage(string message) - { - if (_logTextBox.InvokeRequired) - { - _logTextBox.BeginInvoke(() => LogMessage(message)); - return; - } - - _logTextBox.AppendText(message + Environment.NewLine); - _logTextBox.SelectionStart = _logTextBox.TextLength; - _logTextBox.ScrollToCaret(); - } - - private static Label MakeLabel(string text, Point location, int width) => new() - { - Text = text, - Location = location, - Size = new Size(width, 22), - ForeColor = Color.LightGray, - Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, - }; - - private static Button MakeButton(string text, Point location, Size size, Color back) - { - var button = new Button - { - Text = text, - Location = location, - Size = size, - BackColor = back, - ForeColor = Color.White, - FlatStyle = FlatStyle.Flat, - Cursor = Cursors.Hand, - }; - button.FlatAppearance.BorderColor = Color.FromArgb(80, 80, 80); - return button; - } -} diff --git a/Shigure.Installer/Program.cs b/Shigure.Installer/Program.cs deleted file mode 100644 index 92a7138e..00000000 --- a/Shigure.Installer/Program.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace Shigure.Installer; - -using System.Runtime.InteropServices; - -internal static class Program -{ - [STAThread] - private static int Main(string[] args) - { - // 带参数时走命令行模式(便于脚本化 / 批量生成),无参数时打开图形界面。 - // Shigure.Installer.exe <程序名> [输出目录] - if (args.Length > 0) - { - // WinExe 默认不连接父控制台,附加后命令行输出才可见。 - AttachConsole(AttachParentProcess); - return RunHeadless(args); - } - - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.SetHighDpiMode(HighDpiMode.SystemAware); - Application.Run(new InstallerForm()); - return 0; - } - - private static int RunHeadless(string[] args) - { - var positional = new List(); - - foreach (var arg in args) - { - if (arg is "--help" or "-h" or "/?") - { - PrintUsage(); - return 0; - } - - positional.Add(arg); - } - - if (positional.Count == 0) - { - PrintUsage(); - return 1; - } - - var programName = positional[0]; - var baseDir = positional.Count > 1 - ? positional[1] - : Environment.GetFolderPath(Environment.SpecialFolder.Desktop); - var outputDir = Path.Combine(baseDir, programName); - - try - { - var builder = new ProjectBuilder(Console.WriteLine); - builder.BuildAndPackage(programName, outputDir); - Console.WriteLine($"\n✓ 构建完成:{outputDir}"); - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine($"\n✗ 构建失败:{ex.Message}"); - return 1; - } - } - - private static void PrintUsage() - { - Console.WriteLine("用法:Shigure.Installer.exe <程序名> [输出目录]"); - Console.WriteLine(" <程序名> 仅字母/数字/下划线,不能以数字开头"); - Console.WriteLine(" [输出目录] 可选,默认桌面;最终输出到 <输出目录>\\<程序名>"); - Console.WriteLine("不带任何参数运行则打开图形界面。"); - } - - private const int AttachParentProcess = -1; - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool AttachConsole(int dwProcessId); -} diff --git a/Shigure.Installer/ProjectBuilder.cs b/Shigure.Installer/ProjectBuilder.cs deleted file mode 100644 index f60b8b5b..00000000 --- a/Shigure.Installer/ProjectBuilder.cs +++ /dev/null @@ -1,231 +0,0 @@ -using System.Diagnostics; -using System.Text; -using System.Text.RegularExpressions; - -namespace Shigure.Installer; - -/// -/// 项目构建器:把主项目源码复制到临时目录后,用命令行覆盖程序集名称发布,最后打包到输出目录。 -/// -/// 设计要点(旧版「经常出错」的根因都在这里规避): -/// - 全程在 %TEMP% 的隔离副本里构建,绝不修改主项目的 .csproj / 源码 / bin / obj, -/// 也不依赖「备份-还原」这种崩溃即损坏的机制。 -/// - 程序集改名通过命令行属性 -p:AssemblyName=... 覆盖,而非改写 .csproj 文本。 -/// - 复制时排除 bin / obj / 安装器自身等目录,临时副本天然干净,不会出现重复特性(CS0579)。 -/// - 调用进程一律用 ,自动处理含空格的路径(仓库路径含 "VS Code")。 -/// -internal sealed class ProjectBuilder -{ - private readonly Action _log; - - /// 复制源码时按目录名跳过(任意层级),避免拷入构建产物与无关目录。 - private static readonly HashSet ExcludedDirectories = new(StringComparer.OrdinalIgnoreCase) - { - "bin", "obj", ".git", ".vs", ".vscode", "cache", "artifacts", "tmp", "Shigure.Installer", "node_modules" - }; - - /// 输出包中必须存在的「运行时数据」文件夹(publish 已复制内容,此处保底确保目录存在)。 - private static readonly string[] DataDirectories = { "config", "keymap", "module" }; - - /// 合法程序集名称:仅字母/数字/下划线,且不能以数字开头。GUI 与命令行共用此校验。 - private static readonly Regex NamePattern = new("^[A-Za-z_][A-Za-z0-9_]*$", RegexOptions.Compiled); - - public ProjectBuilder(Action log) => _log = log; - - /// 校验自定义程序名称是否合法。 - public static bool IsValidProgramName(string? name) => - !string.IsNullOrEmpty(name) && NamePattern.IsMatch(name); - - /// - /// 完整流程:定位主项目 → 复制到临时目录 → 发布(改名) → 打包到输出目录 → 清理临时目录。 - /// - /// 自定义程序集名称。 - /// 最终输出目录(会被重建)。 - public void BuildAndPackage(string programName, string outputDir) - { - if (!IsValidProgramName(programName)) - throw new ArgumentException("程序名称不合法:只能包含英文字母、数字、下划线,且不能以数字开头。", nameof(programName)); - - var csprojPath = LocateMainProject(); - var projectDir = Path.GetDirectoryName(csprojPath)!; - _log($"主项目:{csprojPath}"); - - var workRoot = Path.Combine(Path.GetTempPath(), "ShigureInstaller", Guid.NewGuid().ToString("N")); - var srcDir = Path.Combine(workRoot, "src"); - var publishDir = Path.Combine(workRoot, "publish"); - - try - { - _log("① 复制源码到临时目录(排除 bin/obj 等)…"); - CopyDirectory(projectDir, srcDir); - - _log($"② 发布并改名为 {programName}(dotnet publish)…"); - Publish(Path.Combine(srcDir, Path.GetFileName(csprojPath)), publishDir, programName); - - _log("③ 打包到输出目录…"); - PackageOutput(publishDir, outputDir); - } - finally - { - _log("④ 清理临时文件…"); - TryDeleteDirectory(workRoot); - } - } - - /// - /// 从安装器所在位置逐级向上查找主项目 Shigure.csproj。 - /// 安装器自身是 Shigure.Installer.csproj,不会被误匹配。 - /// - private static string LocateMainProject() - { - var dir = new DirectoryInfo(AppContext.BaseDirectory); - for (var i = 0; i < 12 && dir != null; i++, dir = dir.Parent) - { - var candidate = Path.Combine(dir.FullName, "Shigure.csproj"); - if (File.Exists(candidate)) - return candidate; - } - - throw new FileNotFoundException( - "未能从安装器所在位置向上找到主项目 Shigure.csproj。\n" + - "请确认安装器位于 Shigure 仓库的 Shigure.Installer 子目录内运行。"); - } - - /// 递归复制目录,跳过 以及 *.backup / *.user 文件。 - private static void CopyDirectory(string sourceDir, string destDir) - { - Directory.CreateDirectory(destDir); - - foreach (var file in Directory.EnumerateFiles(sourceDir)) - { - var name = Path.GetFileName(file); - if (name.EndsWith(".backup", StringComparison.OrdinalIgnoreCase) || - name.EndsWith(".user", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - File.Copy(file, Path.Combine(destDir, name), overwrite: true); - } - - foreach (var subDir in Directory.EnumerateDirectories(sourceDir)) - { - var name = Path.GetFileName(subDir); - if (ExcludedDirectories.Contains(name)) - continue; - - CopyDirectory(subDir, Path.Combine(destDir, name)); - } - } - - /// 在临时副本上执行 dotnet publish,用 -p:AssemblyName 覆盖程序集名称。 - private void Publish(string csprojPath, string publishDir, string programName) - { - var startInfo = new ProcessStartInfo - { - FileName = "dotnet", - WorkingDirectory = Path.GetDirectoryName(csprojPath)!, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8, - }; - - startInfo.ArgumentList.Add("publish"); - startInfo.ArgumentList.Add(csprojPath); - startInfo.ArgumentList.Add("-c"); - startInfo.ArgumentList.Add("Release"); - startInfo.ArgumentList.Add("-o"); - startInfo.ArgumentList.Add(publishDir); - startInfo.ArgumentList.Add($"-p:AssemblyName={programName}"); - startInfo.ArgumentList.Add("--nologo"); - - using var process = new Process { StartInfo = startInfo }; - - // 收集疑似错误行,构建失败时回传给用户,避免日志被淹没后看不到原因。 - var errorLines = new List(); - - void Handle(string? line, bool isError) - { - if (string.IsNullOrEmpty(line)) - return; - - var looksLikeError = isError || - line.Contains("error", StringComparison.OrdinalIgnoreCase) || - line.Contains("错误"); - - if (looksLikeError && errorLines.Count < 20) - errorLines.Add(line); - - _log((isError ? " ! " : " ") + line); - } - - process.OutputDataReceived += (_, e) => Handle(e.Data, isError: false); - process.ErrorDataReceived += (_, e) => Handle(e.Data, isError: true); - - try - { - process.Start(); - } - catch (Exception ex) - { - throw new InvalidOperationException( - "无法启动 dotnet。请确认已安装 .NET 10 SDK 且 dotnet 在 PATH 中。\n" + ex.Message, ex); - } - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - - if (process.ExitCode != 0) - { - var detail = errorLines.Count > 0 - ? "\n\n" + string.Join("\n", errorLines) - : string.Empty; - throw new InvalidOperationException($"dotnet publish 失败(退出码 {process.ExitCode})。{detail}"); - } - } - - /// 把发布产物复制到输出目录,保留 config/keymap/module 内容。 - private void PackageOutput(string publishDir, string outputDir) - { - if (Directory.Exists(outputDir)) - { - try - { - Directory.Delete(outputDir, recursive: true); - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) - { - throw new IOException( - $"无法清理已存在的输出目录:{outputDir}\n" + - "可能有文件被占用(如该程序正在运行,或文件夹在资源管理器中打开)。请关闭后重试。", ex); - } - } - - CopyDirectory(publishDir, outputDir); - - foreach (var name in DataDirectories) - Directory.CreateDirectory(Path.Combine(outputDir, name)); - - _log("已包含 config/keymap/module 配置内容"); - } - - /// 尽力删除临时目录;失败不抛出(临时文件残留无伤大雅)。 - private void TryDeleteDirectory(string dir) - { - if (!Directory.Exists(dir)) - return; - - try - { - Directory.Delete(dir, recursive: true); - } - catch (Exception ex) - { - _log($" (提示)临时目录未能完全清理:{dir}({ex.Message})"); - } - } -} diff --git a/Shigure.Installer/README.md b/Shigure.Installer/README.md deleted file mode 100644 index 67a8f075..00000000 --- a/Shigure.Installer/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Shigure 安装器 - -一键生成「改名后的定制版」Shigure 程序包。详细说明见 [USAGE.md](USAGE.md)。 - -## 使用方法 - -### 图形界面 - -1. 运行 `Shigure.Installer.exe` -2. 输入自定义程序名称(字母/数字/下划线,不能以数字开头) -3. 选择输出路径(留空默认桌面) -4. 点击「开始构建」 - -### 命令行 - -```powershell -Shigure.Installer.exe <程序名> [输出目录] -``` - -## 工作原理 - -安装器**全程在系统临时目录里构建**,不会修改主项目的 `.csproj`、源码、`bin/`、`obj/`: - -1. 向上定位主项目 `Shigure.csproj` -2. 把源码复制到临时目录(排除 `bin/obj` 等) -3. 在副本上执行 `dotnet publish -c Release -p:AssemblyName=<程序名>`(命令行覆盖程序集名称,不改写文件) -4. 把产物复制到输出目录(包含 `config/keymap/module` 配置内容) -5. 删除临时目录 - -只覆盖 `AssemblyName`、保持 `RootNamespace=Shigure`,以免窗体资源加载错位;生成程序内部命名空间仍为 `Shigure`,仅文件名改变。 - -## 构建安装器 - -```powershell -cd Shigure.Installer -dotnet build -c Release -``` - -生成于:`bin\Release\net10.0-windows\Shigure.Installer.exe` - -## 注意事项 - -- 需要 .NET 10 SDK -- 安装器须在仓库的 `Shigure.Installer` 子目录内运行(用于定位主项目) -- 输出同名目录会被重建;构建前请关闭正在运行的目标程序 diff --git a/Shigure.Installer/Shigure.Installer.csproj b/Shigure.Installer/Shigure.Installer.csproj deleted file mode 100644 index 0050cebe..00000000 --- a/Shigure.Installer/Shigure.Installer.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - WinExe - net10.0-windows - enable - true - enable - Shigure.Installer - Shigure.Installer - - - diff --git a/Shigure.Installer/USAGE.md b/Shigure.Installer/USAGE.md deleted file mode 100644 index 3e21b126..00000000 --- a/Shigure.Installer/USAGE.md +++ /dev/null @@ -1,134 +0,0 @@ -# 安装器使用指南 - -## 🎯 功能说明 - -`Shigure.Installer` 用于一键生成「改名后的定制版」Shigure 程序包。它会: - -1. ✏️ 把程序集名称从 `Shigure` 改为你指定的名称(如 `MyWowHelper.exe`) -2. 🔨 自动编译并发布(Release) -3. 📦 输出一个干净、可直接分发的程序文件夹(包含 `config/keymap/module` 配置内容) - -> **核心特性:全程在系统临时目录里构建,绝不改动主项目的 `.csproj`、源码、`bin/`、`obj/`。** -> 旧版安装器靠「改写 csproj → 还原」「删除 bin/obj」工作,一旦中途出错就会损坏主项目(例如把 `AssemblyName` 写坏)。新版彻底不碰主项目,构建失败也只影响临时目录。 - ---- - -## 📋 前置要求 - -- 已安装 **.NET 10 SDK**(`dotnet --version` 可用) -- Windows 系统 -- 安装器需位于 Shigure 仓库的 `Shigure.Installer` 子目录内运行(它会自动向上定位主项目 `Shigure.csproj`) - ---- - -## 🚀 快速开始(图形界面) - -### 1. 构建安装器 - -```powershell -cd Shigure.Installer -dotnet build -c Release -``` - -安装器位于:`bin\Release\net10.0-windows\Shigure.Installer.exe` - -### 2. 运行并填写 - -双击 `Shigure.Installer.exe`: - -1. **程序名称**:自定义名称(如 `MyWowHelper`) - - 仅支持英文字母、数字、下划线,且不能以数字开头 -2. **输出路径**:选择输出文件夹,留空默认桌面 - - 最终会输出到 `<输出路径>\<程序名称>\` -3. 点击「**开始构建**」,日志区会实时显示进度 - ---- - -## 💻 命令行模式(可脚本化 / 批量生成) - -带参数运行即进入命令行模式,无参数则打开图形界面: - -```powershell -Shigure.Installer.exe <程序名> [输出目录] -``` - -| 参数 | 说明 | -| ------------- | ---------------------------------------------------------- | -| `<程序名>` | 必填,仅字母/数字/下划线,不能以数字开头 | -| `[输出目录]` | 可选,默认桌面;最终输出到 `<输出目录>\<程序名>` | -| `--help` | 显示用法 | - -示例: - -```powershell -.\bin\Release\net10.0-windows\Shigure.Installer.exe DruidHelper D:\Games -``` - -退出码:`0` 成功,非 `0` 失败(失败原因写入标准错误)。 - ---- - -## 📂 输出结构 - -``` -MyWowHelper/ -├── MyWowHelper.exe # 主程序(已改名) -├── MyWowHelper.dll -├── MyWowHelper.deps.json -├── MyWowHelper.runtimeconfig.json -├── *.dll # 运行时依赖 -├── config/ # 配置文件 -├── keymap/ # 按键映射文件 -└── module/ # 模块文件 -``` - ---- - -## ⚙️ 构建流程 - -``` -1. 向上定位主项目 Shigure.csproj -2. 把主项目源码复制到 %TEMP%\ShigureInstaller\<随机>\src - (排除 bin/obj/.git/.vs/.vscode/cache/artifacts/tmp/Shigure.Installer 等) -3. 在临时副本上执行: - dotnet publish src\Shigure.csproj -c Release -o -p:AssemblyName=<程序名> - —— 用命令行属性覆盖程序集名称,不改写任何文件 -4. 把发布产物复制到输出目录(包含 config/keymap/module 配置内容) -5. 删除临时目录 -``` - -### 为什么只改 `AssemblyName`、不改 `RootNamespace` - -代码里的命名空间始终是 `namespace Shigure`,而窗体资源(如 `UI/MainForm.resx`)按 `RootNamespace`(= `Shigure`)生成清单名。若同时改动 `RootNamespace`,运行时会出现资源加载错位。因此只覆盖程序集名称:`.exe/.dll` 改名,代码与资源不受影响。 - ---- - -## ⚠️ 注意事项 - -1. **不影响开发环境**:构建只在临时目录进行,主项目的 `.csproj/源码/bin/obj` 不会被修改。 -2. **输出目录会被重建**:若同名目录已存在会先删除再生成;请勿让目标程序处于运行中或在资源管理器里打开,否则删除会因占用失败(安装器会给出明确提示)。 -3. **命名空间不变**:生成程序内部仍为 `namespace Shigure`,仅程序集(文件)名改变。 - ---- - -## 🔧 故障排查 - -### 提示「无法启动 dotnet」 - -确认 .NET 10 SDK 已安装且在 PATH 中: - -```powershell -dotnet --version -``` - -### 提示「未能找到主项目 Shigure.csproj」 - -安装器从自身所在位置逐级向上查找。请确保它位于仓库的 `Shigure.Installer` 子目录内(即 `Shigure\Shigure.Installer\...`)运行,不要把 exe 单独拷到别处。 - -### 提示「无法清理已存在的输出目录」 - -目标文件夹里有文件被占用:关闭正在运行的同名程序、关掉在资源管理器中打开的该目录,然后重试。 - -### `dotnet publish 失败` - -日志区/标准错误会附带前若干条编译错误。常见为主项目本身存在编译错误——先在仓库根目录确认 `dotnet build .\Shigure.csproj` 能干净通过。 diff --git a/UI/ConditionEditorForm.cs b/UI/ConditionEditorForm.cs index fa0b8875..a3b7428d 100644 --- a/UI/ConditionEditorForm.cs +++ b/UI/ConditionEditorForm.cs @@ -159,17 +159,36 @@ public sealed class ConditionEditorForm : Form private readonly IReadOnlyList _fields; private readonly string _originalCondition; + private readonly bool _allowSubConditions; private readonly FlowLayoutPanel _rowsPanel = new(); private readonly Label _previewLabel = new(); private readonly ToolTip _previewToolTip = new(); private readonly List _rows = new(); + private readonly List _subConditions = new(); + private readonly ListBox _subList = new(); public string ConditionText { get; private set; } = string.Empty; - public ConditionEditorForm(IReadOnlyList fields, string? condition) + // 子条件: 与主条件是「且」、子条件彼此是「或」。allowSubConditions=false(默认)时不显示该区, + // 也用于子条件自身的嵌套编辑弹窗防止无限递归。 + public IReadOnlyList SubConditions => _subConditions; + + public ConditionEditorForm( + IReadOnlyList fields, + string? condition, + IEnumerable? subConditions = null, + bool allowSubConditions = false) { _fields = fields; _originalCondition = condition ?? string.Empty; + _allowSubConditions = allowSubConditions; + if (subConditions is not null) + { + _subConditions.AddRange(subConditions + .Select(sub => sub?.Trim() ?? string.Empty) + .Where(sub => sub.Length > 0)); + } + InitializeComponent(); foreach (var term in ConditionExpression.Parse(condition)) @@ -200,7 +219,9 @@ public sealed class ConditionEditorForm : Form BackColor = UiTheme.Background; ForeColor = UiTheme.Text; // 加一个滚动条宽度, 避免行数多时垂直滚动条盖住每行的 ✕ 删除按钮。 - ClientSize = new Size(RowTotalWidth + 50 + SystemInformation.VerticalScrollBarWidth, 460); + // 子条件区会额外占高度, 允许时把窗口加高, 给主条件行留出空间。 + var initialHeight = _allowSubConditions ? 650 : 460; + ClientSize = new Size(RowTotalWidth + 50 + SystemInformation.VerticalScrollBarWidth, initialHeight); FormBorderStyle = FormBorderStyle.Sizable; MaximizeBox = false; MinimizeBox = false; @@ -214,16 +235,13 @@ public sealed class ConditionEditorForm : Form Dock = DockStyle.Fill, BackColor = UiTheme.Background, Padding = new Padding(12, 10, 12, 10), - ColumnCount = 1, - RowCount = 4 + ColumnCount = 1 }; - root.RowStyles.Add(new RowStyle(SizeType.Absolute, 26)); - root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); - root.RowStyles.Add(new RowStyle(SizeType.Absolute, 28)); - root.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); Controls.Add(root); - root.Controls.Add(BuildHeaderRow(), 0, 0); + var rowIndex = 0; + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 26)); + root.Controls.Add(BuildHeaderRow(), 0, rowIndex++); _rowsPanel.Dock = DockStyle.Fill; _rowsPanel.BackColor = UiTheme.SurfaceRaised; @@ -232,16 +250,168 @@ public sealed class ConditionEditorForm : Form _rowsPanel.AutoScroll = true; _rowsPanel.Margin = new Padding(0, 4, 0, 6); _rowsPanel.Padding = new Padding(8, 6, 8, 6); - root.Controls.Add(_rowsPanel, 0, 1); + root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + root.Controls.Add(_rowsPanel, 0, rowIndex++); + + // 「添加条件」紧贴主条件行下方, 明确它作用于上面的主条件(而非子条件)。 + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 38)); + root.Controls.Add(BuildAddConditionRow(), 0, rowIndex++); + + if (_allowSubConditions) + { + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 172)); + root.Controls.Add(BuildSubConditionsPanel(), 0, rowIndex++); + } _previewLabel.Dock = DockStyle.Fill; _previewLabel.ForeColor = UiTheme.Muted; _previewLabel.TextAlign = ContentAlignment.MiddleLeft; _previewLabel.AutoEllipsis = true; _previewLabel.Margin = new Padding(0); - root.Controls.Add(_previewLabel, 0, 2); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 28)); + root.Controls.Add(_previewLabel, 0, rowIndex++); - root.Controls.Add(BuildActionRow(), 0, 3); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); + root.Controls.Add(BuildActionRow(), 0, rowIndex++); + root.RowCount = rowIndex; + } + + // 子条件区: 标题 + 暗色列表 + 添加/编辑/删除。每条子条件本身也是一条完整条件, + // 通过嵌套的(无子条件区的)条件编辑弹窗来编辑。 + private Control BuildSubConditionsPanel() + { + var panel = new TableLayoutPanel + { + Dock = DockStyle.Fill, + BackColor = UiTheme.Background, + Margin = new Padding(0, 4, 0, 4), + ColumnCount = 2, + RowCount = 2 + }; + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 116)); + panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 22)); + panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + + var title = new Label + { + Text = "子条件 (满足任一即可, 与主条件为「且」关系)", + Dock = DockStyle.Fill, + ForeColor = UiTheme.Muted, + TextAlign = ContentAlignment.MiddleLeft, + Margin = new Padding(0) + }; + panel.Controls.Add(title, 0, 0); + panel.SetColumnSpan(title, 2); + + _subList.Dock = DockStyle.Fill; + _subList.BackColor = UiTheme.Field; + _subList.ForeColor = UiTheme.Text; + _subList.BorderStyle = BorderStyle.FixedSingle; + _subList.IntegralHeight = false; + _subList.Margin = new Padding(0, 0, 8, 0); + _subList.DoubleClick += (_, _) => EditSelectedSubCondition(); + panel.Controls.Add(_subList, 0, 1); + + var buttons = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.TopDown, + WrapContents = false, + BackColor = UiTheme.Background, + Margin = new Padding(0) + }; + buttons.Controls.Add(CreateSubButton("添加子条件", UiTheme.Text, AddSubCondition)); + buttons.Controls.Add(CreateSubButton("编辑", UiTheme.Text, EditSelectedSubCondition)); + buttons.Controls.Add(CreateSubButton("删除", UiTheme.Danger, DeleteSelectedSubCondition)); + panel.Controls.Add(buttons, 1, 1); + + RefreshSubList(); + return panel; + } + + private static Button CreateSubButton(string text, Color foreColor, Action onClick) + { + var button = UiTheme.CreateButton(text, UiTheme.Field, foreColor); + button.Width = 108; + button.Height = 30; + button.Margin = new Padding(0, 0, 0, 6); + button.Click += (_, _) => onClick(); + return button; + } + + private void AddSubCondition() + { + var text = PromptSubCondition(string.Empty); + if (string.IsNullOrWhiteSpace(text)) + { + return; + } + + _subConditions.Add(text.Trim()); + RefreshSubList(); + _subList.SelectedIndex = _subConditions.Count - 1; + UpdatePreview(); + } + + private void EditSelectedSubCondition() + { + var index = _subList.SelectedIndex; + if (index < 0 || index >= _subConditions.Count) + { + return; + } + + var text = PromptSubCondition(_subConditions[index]); + if (text is null) + { + return; + } + + // 编辑后清空 = 删除该子条件。 + if (string.IsNullOrWhiteSpace(text)) + { + _subConditions.RemoveAt(index); + } + else + { + _subConditions[index] = text.Trim(); + } + + RefreshSubList(); + UpdatePreview(); + } + + private void DeleteSelectedSubCondition() + { + var index = _subList.SelectedIndex; + if (index < 0 || index >= _subConditions.Count) + { + return; + } + + _subConditions.RemoveAt(index); + RefreshSubList(); + UpdatePreview(); + } + + // 返回 null = 用户取消; 空串 = 用户清空了条件(编辑时表示删除)。 + private string? PromptSubCondition(string current) + { + using var editor = new ConditionEditorForm(_fields, current); + return editor.ShowDialog(this) == DialogResult.OK ? editor.ConditionText : null; + } + + private void RefreshSubList() + { + _subList.BeginUpdate(); + _subList.Items.Clear(); + foreach (var sub in _subConditions) + { + _subList.Items.Add(sub); + } + + _subList.EndUpdate(); } private Control BuildHeaderRow() @@ -271,6 +441,34 @@ public sealed class ConditionEditorForm : Form return header; } + // 「添加条件」按钮单独成行, 放在主条件行下方、子条件区上方。 + private Control BuildAddConditionRow() + { + var panel = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.LeftToRight, + WrapContents = false, + BackColor = UiTheme.Background, + Margin = new Padding(0, 2, 0, 2) + }; + + var addButton = UiTheme.CreateButton("添加条件", UiTheme.Field, UiTheme.Text); + addButton.Width = 96; + addButton.Height = 30; + addButton.Margin = new Padding(0, 2, 0, 0); + addButton.Click += (_, _) => + { + var row = AddRow(null); + RefreshConnectors(); + UpdatePreview(); + _rowsPanel.ScrollControlIntoView(row.Panel); + row.CategoryBox.Focus(); + }; + panel.Controls.Add(addButton); + return panel; + } + private Control BuildActionRow() { var row = new TableLayoutPanel @@ -284,29 +482,6 @@ public sealed class ConditionEditorForm : Form row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 170)); - var leftButtons = new FlowLayoutPanel - { - Dock = DockStyle.Fill, - FlowDirection = FlowDirection.LeftToRight, - WrapContents = false, - BackColor = UiTheme.Background, - Margin = new Padding(0) - }; - var addButton = UiTheme.CreateButton("添加条件", UiTheme.Field, UiTheme.Text); - addButton.Width = 96; - addButton.Height = 30; - addButton.Margin = new Padding(0, 4, 0, 0); - addButton.Click += (_, _) => - { - var row = AddRow(null); - RefreshConnectors(); - UpdatePreview(); - _rowsPanel.ScrollControlIntoView(row.Panel); - row.CategoryBox.Focus(); - }; - leftButtons.Controls.Add(addButton); - row.Controls.Add(leftButtons, 0, 0); - var rightButtons = new FlowLayoutPanel { Dock = DockStyle.Fill, @@ -352,8 +527,9 @@ public sealed class ConditionEditorForm : Form } var text = ConditionExpression.Build(CollectTerms()); - // 仅当原本有条件、现在变空时才提醒(避免把已有规则误清成"始终命中")。 + // 仅当原本有条件、现在主条件与子条件都为空时才提醒(避免把已有规则误清成"始终命中")。 if (text.Length == 0 + && _subConditions.Count == 0 && !string.IsNullOrWhiteSpace(_originalCondition) && MessageBox.Show( "当前条件为空, 将清除该规则的条件(始终命中)。继续?", @@ -708,10 +884,22 @@ public sealed class ConditionEditorForm : Form private void UpdatePreview() { - var text = ConditionExpression.Build(CollectTerms()); - _previewLabel.Text = text.Length == 0 ? "预览: (无条件, 始终命中)" : $"预览: {text}"; + var full = ComposePreview(ConditionExpression.Build(CollectTerms())); + _previewLabel.Text = full.Length == 0 ? "预览: (无条件, 始终命中)" : $"预览: {full}"; // 单行预览会被省略号截断, 悬停看完整表达式。 - _previewToolTip.SetToolTip(_previewLabel, text.Length == 0 ? string.Empty : text); + _previewToolTip.SetToolTip(_previewLabel, full.Length == 0 ? string.Empty : full); + } + + // 把主条件文本与子条件合成为可读的整体表达式(与 ModuleRule.DescribeCondition 同形)。 + private string ComposePreview(string mainText) + { + if (_subConditions.Count == 0) + { + return mainText; + } + + var any = string.Join(" | ", _subConditions); + return mainText.Length == 0 ? $"任一({any})" : $"{mainText} 且任一({any})"; } private static bool IsFalseText(string? value) diff --git a/UI/MainForm.cs b/UI/MainForm.cs index 6c0924ae..f869b44d 100644 --- a/UI/MainForm.cs +++ b/UI/MainForm.cs @@ -5,7 +5,7 @@ namespace Shigure; public sealed class MainForm : Form, IMessageFilter { private const int ResizeGripSize = 8; - private const string HeaderIconResourceName = "Shigure.Assets.arasaka-icon-transparent.png"; + private const string HeaderIconResourcePath = "Assets.arasaka-icon-transparent.png"; private static readonly Color DefaultHeaderIconColor = Color.White; private static readonly IReadOnlyDictionary ClassIconColors = new Dictionary { @@ -290,7 +290,7 @@ public sealed class MainForm : Form, IMessageFilter private static Bitmap? LoadHeaderIconMask() { - using var stream = typeof(MainForm).Assembly.GetManifestResourceStream(HeaderIconResourceName); + using var stream = typeof(MainForm).Assembly.GetManifestResourceStream(GetHeaderIconResourceName()); if (stream is null) { return null; @@ -300,6 +300,9 @@ public sealed class MainForm : Form, IMessageFilter return new Bitmap(image); } + private static string GetHeaderIconResourceName() => + $"{typeof(MainForm).Namespace}.{HeaderIconResourcePath}"; + private static Bitmap TintHeaderIcon(Bitmap mask, Color color) { var bitmap = new Bitmap(mask.Width, mask.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); diff --git a/UI/ModuleEditorControl.cs b/UI/ModuleEditorControl.cs index c7bfc545..7cda21ff 100644 --- a/UI/ModuleEditorControl.cs +++ b/UI/ModuleEditorControl.cs @@ -1222,7 +1222,7 @@ public sealed class ModuleEditorControl : UserControl Count } - private sealed record RuleRowValues(bool Enabled, string Spell, string UnitText, string Condition); + private sealed record RuleRowValues(bool Enabled, string Spell, string UnitText, string Condition, IReadOnlyList SubConditions); private void ApplyColumnWidths(Dictionary? widths) { @@ -1323,13 +1323,33 @@ public sealed class ModuleEditorControl : UserControl return; } - if (_rulesGrid.Columns[e.ColumnIndex].Name != "RuleNumber") + var columnName = _rulesGrid.Columns[e.ColumnIndex].Name; + var row = _rulesGrid.Rows[e.RowIndex]; + if (columnName == "RuleNumber") { + e.Value = row.IsNewRow ? string.Empty : (e.RowIndex + 1).ToString(); + e.FormattingApplied = true; return; } - e.Value = _rulesGrid.Rows[e.RowIndex].IsNewRow ? string.Empty : (e.RowIndex + 1).ToString(); - e.FormattingApplied = true; + // 「条件」列在有子条件时显示成 "主条件 且任一(子1 | 子2)"; 仅改显示, 底层值仍是主条件, 不影响 ReadRules 存盘。 + if (columnName == "Condition" && !row.IsNewRow) + { + e.Value = DecorateCondition(e.Value?.ToString() ?? string.Empty, row.Tag as List); + e.FormattingApplied = true; + } + } + + // 把主条件与子条件合成可读文本(与 ModuleRule.DescribeCondition / 弹窗预览同形)。无子条件时原样返回。 + private static string DecorateCondition(string main, List? subs) + { + if (subs is not { Count: > 0 }) + { + return main; + } + + var any = string.Join(" | ", subs); + return main.Length == 0 ? $"任一({any})" : $"{main} 且任一({any})"; } private void OnRulesGridCellPainting(object? sender, DataGridViewCellPaintingEventArgs e) @@ -1427,10 +1447,16 @@ public sealed class ModuleEditorControl : UserControl return string.Empty; } - var text = CellText(_rulesGrid.Rows[rowIndex], columnName); - if (columnName == "Condition" && text.Length == 0) + var row = _rulesGrid.Rows[rowIndex]; + var text = CellText(row, columnName); + if (columnName == "Condition") { - return "点击编辑条件 (当前: 始终命中)"; + // 提示与裁剪检测都用合成后的完整文本(含子条件), 与单元格显示一致。 + text = DecorateCondition(text, row.Tag as List); + if (text.Length == 0) + { + return "点击编辑条件 (当前: 始终命中)"; + } } return IsCellTextClipped(text, columnIndex) ? text : string.Empty; @@ -1762,7 +1788,7 @@ public sealed class ModuleEditorControl : UserControl return; } - InsertRuleAfter(rowIndex, new RuleRowValues(true, string.Empty, string.Empty, string.Empty)); + InsertRuleAfter(rowIndex, new RuleRowValues(true, string.Empty, string.Empty, string.Empty, Array.Empty())); } private void InsertRuleAfter(int rowIndex, RuleRowValues values) @@ -1971,7 +1997,9 @@ public sealed class ModuleEditorControl : UserControl CellBool(row, "Enabled", defaultValue: true), CellText(row, "Spell"), CellText(row, "Unit"), - CellText(row, "Condition")); + CellText(row, "Condition"), + // 子条件挂在 row.Tag, 随行一起被移动/拖拽/复制搬运。 + row.Tag as List ?? new List()); } private void WriteRuleRow(DataGridViewRow row, RuleRowValues values) @@ -1980,6 +2008,7 @@ public sealed class ModuleEditorControl : UserControl EnsureComboItem(_spellColumn, values.Spell); row.Cells["Spell"].Value = values.Spell; row.Cells["Condition"].Value = values.Condition; + row.Tag = new List(values.SubConditions); RebuildUnitCell(row, values.UnitText); } @@ -1987,26 +2016,32 @@ public sealed class ModuleEditorControl : UserControl { var row = _rulesGrid.Rows[rowIndex]; var current = row.IsNewRow ? string.Empty : CellText(row, "Condition"); + var currentSubs = row.IsNewRow ? null : row.Tag as List; var fields = BuildConditionFields(); - using var editor = new ConditionEditorForm(fields, current); + using var editor = new ConditionEditorForm(fields, current, currentSubs, allowSubConditions: true); if (editor.ShowDialog(FindForm()) != DialogResult.OK) { return; } + var subs = new List(editor.SubConditions); if (row.IsNewRow) { - // 新行占位符不能直接赋值, 改为追加一行。 - if (!string.IsNullOrWhiteSpace(editor.ConditionText)) + // 新行占位符不能直接赋值, 改为追加一行(主条件或子条件任一非空即可)。 + if (!string.IsNullOrWhiteSpace(editor.ConditionText) || subs.Count > 0) { - _rulesGrid.Rows.Add(true, string.Empty, string.Empty, editor.ConditionText); + var index = _rulesGrid.Rows.Add(true, string.Empty, string.Empty, editor.ConditionText); + _rulesGrid.Rows[index].Tag = subs; } return; } row.Cells["Condition"].Value = editor.ConditionText; + row.Tag = subs; + // 让「条件」列的装饰显示(主条件 且任一(…))立即刷新。 + _rulesGrid.InvalidateRow(rowIndex); } // 条件字段 = 状态/技能字段 + 每个动态单位的裸名(存在)和值名称 + 每个数量名。 @@ -2186,6 +2221,9 @@ public sealed class ModuleEditorControl : UserControl EnsureComboItem(_spellColumn, rule.Spell); // 先加行(目标先留空), 再按技能重建目标选项并写回目标值, 避免值不在选项内被吞掉。 var index = _rulesGrid.Rows.Add(rule.Enabled, rule.Spell, string.Empty, rule.Condition); + _rulesGrid.Rows[index].Tag = rule.SubConditions is null + ? new List() + : new List(rule.SubConditions); RebuildUnitCell(_rulesGrid.Rows[index], unitText); } } @@ -2434,6 +2472,10 @@ public sealed class ModuleEditorControl : UserControl // 目标文本命中已定义动态单位名 → UnitName; 否则按数字 → Unit; 都不是则留空。 var isDynamic = unitNames.Contains(unitText); + var subs = (row.Tag as List)? + .Select(sub => sub?.Trim() ?? string.Empty) + .Where(sub => sub.Length > 0) + .ToList(); rules.Add(new ModuleRule { Enabled = CellBool(row, "Enabled", defaultValue: true), @@ -2442,7 +2484,8 @@ public sealed class ModuleEditorControl : UserControl UnitName = isDynamic ? unitText : null, Spell = spell, Hotkey = string.Empty, - Step = string.Empty + Step = string.Empty, + SubConditions = subs is { Count: > 0 } ? subs : null }); } diff --git a/bin/Debug/net10.0-windows/Shigure.dll b/bin/Debug/net10.0-windows/Shigure.dll index e45495aa..3b5fbd2b 100644 Binary files a/bin/Debug/net10.0-windows/Shigure.dll and b/bin/Debug/net10.0-windows/Shigure.dll differ diff --git a/bin/Debug/net10.0-windows/Shigure.exe b/bin/Debug/net10.0-windows/Shigure.exe index 9ae74c67..2765f474 100644 Binary files a/bin/Debug/net10.0-windows/Shigure.exe and b/bin/Debug/net10.0-windows/Shigure.exe differ diff --git a/bin/Debug/net10.0-windows/Shigure.pdb b/bin/Debug/net10.0-windows/Shigure.pdb index 7d09c199..e2b0c789 100644 Binary files a/bin/Debug/net10.0-windows/Shigure.pdb and b/bin/Debug/net10.0-windows/Shigure.pdb differ diff --git a/obj/Debug/net10.0-windows/Shigure.AssemblyInfo.cs b/obj/Debug/net10.0-windows/Shigure.AssemblyInfo.cs index cb01df33..bb2429b3 100644 --- a/obj/Debug/net10.0-windows/Shigure.AssemblyInfo.cs +++ b/obj/Debug/net10.0-windows/Shigure.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Shigure")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+eb53afa819f6248837677fbd6a7287e00936f88c")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e3436299132c3357d55799ec2a2277b733308e71")] [assembly: System.Reflection.AssemblyProductAttribute("Shigure")] [assembly: System.Reflection.AssemblyTitleAttribute("Shigure")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/obj/Debug/net10.0-windows/Shigure.AssemblyInfoInputs.cache b/obj/Debug/net10.0-windows/Shigure.AssemblyInfoInputs.cache index c547285a..48766ed1 100644 --- a/obj/Debug/net10.0-windows/Shigure.AssemblyInfoInputs.cache +++ b/obj/Debug/net10.0-windows/Shigure.AssemblyInfoInputs.cache @@ -1 +1 @@ -f08326bac54400ee633e472a3b677e80ec43484f16839544ee375c26413bfa45 +d6c8cd63f691a3c46a43f86b8c30ac98568cc05222927b6cb2844bb8348b1b94 diff --git a/obj/Debug/net10.0-windows/Shigure.dll b/obj/Debug/net10.0-windows/Shigure.dll index e45495aa..3b5fbd2b 100644 Binary files a/obj/Debug/net10.0-windows/Shigure.dll and b/obj/Debug/net10.0-windows/Shigure.dll differ diff --git a/obj/Debug/net10.0-windows/Shigure.pdb b/obj/Debug/net10.0-windows/Shigure.pdb index 7d09c199..e2b0c789 100644 Binary files a/obj/Debug/net10.0-windows/Shigure.pdb and b/obj/Debug/net10.0-windows/Shigure.pdb differ diff --git a/obj/Debug/net10.0-windows/Shigure.sourcelink.json b/obj/Debug/net10.0-windows/Shigure.sourcelink.json index 0ac28a85..12b79e85 100644 --- a/obj/Debug/net10.0-windows/Shigure.sourcelink.json +++ b/obj/Debug/net10.0-windows/Shigure.sourcelink.json @@ -1 +1 @@ -{"documents":{"C:\\Users\\bianw\\OneDrive\\VS Code\\Shigure\\*":"https://raw.githubusercontent.com/waynebian01/Shigure/eb53afa819f6248837677fbd6a7287e00936f88c/*"}} \ No newline at end of file +{"documents":{"C:\\Users\\bianw\\OneDrive\\VS Code\\Shigure\\*":"https://raw.githubusercontent.com/waynebian01/Shigure/e3436299132c3357d55799ec2a2277b733308e71/*"}} \ No newline at end of file diff --git a/obj/Debug/net10.0-windows/apphost.exe b/obj/Debug/net10.0-windows/apphost.exe index 9ae74c67..2765f474 100644 Binary files a/obj/Debug/net10.0-windows/apphost.exe and b/obj/Debug/net10.0-windows/apphost.exe differ diff --git a/obj/Debug/net10.0-windows/ref/Shigure.dll b/obj/Debug/net10.0-windows/ref/Shigure.dll index 5070a029..424948d1 100644 Binary files a/obj/Debug/net10.0-windows/ref/Shigure.dll and b/obj/Debug/net10.0-windows/ref/Shigure.dll differ diff --git a/obj/Debug/net10.0-windows/refint/Shigure.dll b/obj/Debug/net10.0-windows/refint/Shigure.dll index 5070a029..424948d1 100644 Binary files a/obj/Debug/net10.0-windows/refint/Shigure.dll and b/obj/Debug/net10.0-windows/refint/Shigure.dll differ