This commit is contained in:
waynebian01
2026-06-16 00:21:55 +08:00
parent e343629913
commit 645095f0e4
21 changed files with 376 additions and 799 deletions

View File

@@ -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<string>? 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<string>(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<ModuleRule>();
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<int, string>? 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))

View File

@@ -1,235 +0,0 @@
using System.Diagnostics;
namespace Shigure.Installer;
/// <summary>
/// 安装器主界面:收集程序集名称 / 输出路径 / 是否保留配置,触发隔离构建流程。
/// 实际构建逻辑全部在 <see cref="ProjectBuilder"/>,本类只负责交互与状态。
/// </summary>
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;
}
}

View File

@@ -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<string>();
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);
}

View File

@@ -1,231 +0,0 @@
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
namespace Shigure.Installer;
/// <summary>
/// 项目构建器:把主项目源码复制到临时目录后,用命令行覆盖程序集名称发布,最后打包到输出目录。
///
/// 设计要点(旧版「经常出错」的根因都在这里规避):
/// - 全程在 <c>%TEMP%</c> 的隔离副本里构建,<b>绝不</b>修改主项目的 .csproj / 源码 / bin / obj
/// 也不依赖「备份-还原」这种崩溃即损坏的机制。
/// - 程序集改名通过命令行属性 <c>-p:AssemblyName=...</c> 覆盖,而非改写 .csproj 文本。
/// - 复制时排除 bin / obj / 安装器自身等目录,临时副本天然干净,不会出现重复特性(CS0579)。
/// - 调用进程一律用 <see cref="ProcessStartInfo.ArgumentList"/>,自动处理含空格的路径(仓库路径含 "VS Code")。
/// </summary>
internal sealed class ProjectBuilder
{
private readonly Action<string> _log;
/// <summary>复制源码时按目录名跳过(任意层级),避免拷入构建产物与无关目录。</summary>
private static readonly HashSet<string> ExcludedDirectories = new(StringComparer.OrdinalIgnoreCase)
{
"bin", "obj", ".git", ".vs", ".vscode", "cache", "artifacts", "tmp", "Shigure.Installer", "node_modules"
};
/// <summary>输出包中必须存在的「运行时数据」文件夹publish 已复制内容,此处保底确保目录存在)。</summary>
private static readonly string[] DataDirectories = { "config", "keymap", "module" };
/// <summary>合法程序集名称:仅字母/数字/下划线且不能以数字开头。GUI 与命令行共用此校验。</summary>
private static readonly Regex NamePattern = new("^[A-Za-z_][A-Za-z0-9_]*$", RegexOptions.Compiled);
public ProjectBuilder(Action<string> log) => _log = log;
/// <summary>校验自定义程序名称是否合法。</summary>
public static bool IsValidProgramName(string? name) =>
!string.IsNullOrEmpty(name) && NamePattern.IsMatch(name);
/// <summary>
/// 完整流程:定位主项目 → 复制到临时目录 → 发布(改名) → 打包到输出目录 → 清理临时目录。
/// </summary>
/// <param name="programName">自定义程序集名称。</param>
/// <param name="outputDir">最终输出目录(会被重建)。</param>
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);
}
}
/// <summary>
/// 从安装器所在位置逐级向上查找主项目 <c>Shigure.csproj</c>。
/// 安装器自身是 <c>Shigure.Installer.csproj</c>,不会被误匹配。
/// </summary>
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 子目录内运行。");
}
/// <summary>递归复制目录,跳过 <see cref="ExcludedDirectories"/> 以及 *.backup / *.user 文件。</summary>
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));
}
}
/// <summary>在临时副本上执行 <c>dotnet publish</c>,用 <c>-p:AssemblyName</c> 覆盖程序集名称。</summary>
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<string>();
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}");
}
}
/// <summary>把发布产物复制到输出目录,保留 config/keymap/module 内容。</summary>
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 配置内容");
}
/// <summary>尽力删除临时目录;失败不抛出(临时文件残留无伤大雅)。</summary>
private void TryDeleteDirectory(string dir)
{
if (!Directory.Exists(dir))
return;
try
{
Directory.Delete(dir, recursive: true);
}
catch (Exception ex)
{
_log($" (提示)临时目录未能完全清理:{dir}{ex.Message}");
}
}
}

View File

@@ -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` 子目录内运行(用于定位主项目)
- 输出同名目录会被重建;构建前请关闭正在运行的目标程序

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>Shigure.Installer</AssemblyName>
<RootNamespace>Shigure.Installer</RootNamespace>
</PropertyGroup>
</Project>

View File

@@ -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 <publish> -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` 能干净通过。

View File

@@ -159,17 +159,36 @@ public sealed class ConditionEditorForm : Form
private readonly IReadOnlyList<ConditionField> _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<ConditionRow> _rows = new();
private readonly List<string> _subConditions = new();
private readonly ListBox _subList = new();
public string ConditionText { get; private set; } = string.Empty;
public ConditionEditorForm(IReadOnlyList<ConditionField> fields, string? condition)
// 子条件: 与主条件是「且」、子条件彼此是「或」。allowSubConditions=false(默认)时不显示该区,
// 也用于子条件自身的嵌套编辑弹窗防止无限递归。
public IReadOnlyList<string> SubConditions => _subConditions;
public ConditionEditorForm(
IReadOnlyList<ConditionField> fields,
string? condition,
IEnumerable<string>? 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)

View File

@@ -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<int, Color> ClassIconColors = new Dictionary<int, Color>
{
@@ -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);

View File

@@ -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<string> SubConditions);
private void ApplyColumnWidths(Dictionary<string, int>? 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<string>);
e.FormattingApplied = true;
}
}
// 把主条件与子条件合成可读文本(与 ModuleRule.DescribeCondition / 弹窗预览同形)。无子条件时原样返回。
private static string DecorateCondition(string main, List<string>? 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<string>);
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<string>()));
}
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<string> ?? new List<string>());
}
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<string>(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<string>;
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<string>(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<string>()
: new List<string>(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<string>)?
.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
});
}

View File

@@ -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")]

View File

@@ -1 +1 @@
f08326bac54400ee633e472a3b677e80ec43484f16839544ee375c26413bfa45
d6c8cd63f691a3c46a43f86b8c30ac98568cc05222927b6cb2844bb8348b1b94

View File

@@ -1 +1 @@
{"documents":{"C:\\Users\\bianw\\OneDrive\\VS Code\\Shigure\\*":"https://raw.githubusercontent.com/waynebian01/Shigure/eb53afa819f6248837677fbd6a7287e00936f88c/*"}}
{"documents":{"C:\\Users\\bianw\\OneDrive\\VS Code\\Shigure\\*":"https://raw.githubusercontent.com/waynebian01/Shigure/e3436299132c3357d55799ec2a2277b733308e71/*"}}