feat: 模块目录迁移至用户配置目录

模块存储位置从 exe 旁 baseDirectory/module 迁移到 {UserProfile}/.config/{AppName}/module
(AppName 动态取程序集名,Windows 下为 C:\Users\<用户名>\.config\Shigure\module)。

- ModuleStore.ResolveModuleDirectory 改为无参静态方法,删除 Shigure.csproj 检测死代码
- AppInfo 新增 AppName(程序集名单一来源,与 Version 惯例一致)
- Program.cs 新增启动迁移提示:旧目录有内容且新目录无模块文件时弹纯文案提示
- StatusForm 关于页在路径位于 exe 目录外时显示完整路径;编辑器打开文件夹跟随新路径
- csproj 删除 module 复制项;仓库根 module/ 示例目录保留
- 文档同步更新(README/CLAUDE/打包说明/Obsidian 01/11/00/10)
- 旧模块不自动迁移,由提示引导用户手动移动
This commit is contained in:
liantian-cn
2026-08-15 11:21:10 +08:00
parent 7ed006eab8
commit fd5b04463a
13 changed files with 74 additions and 31 deletions

View File

@@ -19,7 +19,8 @@ internal static class Program
var options = AppOptions.FromArgs(args);
var baseDirectory = AppPaths.BaseDirectory;
var moduleStore = new ModuleStore(ModuleStore.ResolveModuleDirectory(baseDirectory));
var moduleStore = new ModuleStore(ModuleStore.ResolveModuleDirectory());
ShowModuleMigrationHint(baseDirectory, moduleStore);
var triggerKeyState = new WindowsTriggerKeyState();
var processLocator = new WowProcessLocator(baseDirectory);
var runtimeFactory = new ShigureRuntimeFactory(baseDirectory, moduleStore, triggerKeyState, processLocator);
@@ -33,4 +34,37 @@ internal static class Program
processLocator,
runtimeSession));
}
// 模块目录迁移提示:旧目录(baseDirectory/module)存在且有内容、且新目录(用户配置目录)尚无模块文件时,
// 弹一次纯文案提示,引导用户手动移动模块;条件不满足或检测异常时静默,不影响启动。
private static void ShowModuleMigrationHint(string baseDirectory, ModuleStore moduleStore)
{
try
{
var oldDirectory = Path.Combine(baseDirectory, "module");
if (!Directory.Exists(oldDirectory)
|| !Directory.EnumerateFileSystemEntries(oldDirectory).Any())
{
return;
}
// 新目录以是否存在模块文件(.json)判空,避免保存崩溃残留的 .tmp 原子写文件阻断提示。
if (Directory.EnumerateFiles(moduleStore.ModuleDirectory, "*.json", SearchOption.AllDirectories).Any())
{
return;
}
MessageBox.Show(
$"模块目录已迁移到用户配置目录。\n\n" +
$"新的模块目录:\n{moduleStore.ModuleDirectory}\n\n" +
$"请将旧目录 {oldDirectory} 中的模块文件手动移动到新目录。",
"Shigure",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
catch
{
// 检测或提示异常时静默跳过,避免影响启动。
}
}
}

View File

@@ -20,7 +20,7 @@ dotnet run --project .\Shigure.csproj -- --toggle XBUTTON2 --mode switch --logic
- **没有测试项目**:验证 = 能编译 + 实际运行点开「设置」走查。`dotnet build` 干净通过0 警告 0 错误)是基线要求。
- 启动参数见 [README.md](README.md#运行)`--toggle/--mode/--logic-ms/--render-ms`),解析在 [App/AppOptions.cs](App/AppOptions.cs)。目标进程名来自 `wow_process.txt`
程序直接从当前 EXE 所在目录运行;`AppPaths.BaseDirectory``AppContext.BaseDirectory`,配置、按键映射、模块和插件源码均从该目录读取。
程序直接从当前 EXE 所在目录运行;`AppPaths.BaseDirectory``AppContext.BaseDirectory`,配置、按键映射和插件源码均从该目录读取;模块从用户配置目录读取(`ModuleStore.ResolveModuleDirectory()`,见下)
## 架构与数据流
@@ -53,13 +53,14 @@ Input/ keymap 读取、按键发送、Win32 API
Infrastructure/ 配置读取(ConfigService)、JSON 辅助、UI 缓存、路径、Fuyutsui 插件文件读写
UI/ WinForms 界面、编辑器、主题
Fuyutsui/ 内置插件权威源;构建/发布时完整复制,运行时部署到游戏 AddOns
config/ keymap/ module/ 运行时 JSON 数据(构建时复制到输出, 见 .csproj 的 None+CopyToOutputDirectory)
config/ keymap/ 运行时 JSON 数据(构建时复制到输出, 见 .csproj 的 None+CopyToOutputDirectory)
module 运行时模块数据位于用户配置目录 ~/.config/Shigure/module(启动时自动创建, 不随构建复制)
wow_process.txt 目标游戏进程名列表;构建时复制,运行期间每次定位都会重新读取
```
## 模块解析(改逻辑前必读)
- 模块以 `module/模块名.json` 平铺保存,**文件名取自模块名,故模块名不可重复**;加载递归扫描子目录。模型在 [Modules/ModuleStore.cs](Modules/ModuleStore.cs)`ModuleDefinition`/`ModuleMatch`/`ModuleRule`/`ModuleUnit`/`ModuleCountField`/`ModuleValueAdjustment`)。`RecommendedTalent``ModuleDefinition` 上的纯展示字段,不参与匹配(`ModuleMatch.Specificity` 不计入)。
- 模块以 `~/.config/Shigure/module/模块名.json` 平铺保存`ModuleStore.ResolveModuleDirectory()` 返回用户配置目录,`{UserProfile}/.config/{程序集名}/module`**文件名取自模块名,故模块名不可重复**;加载递归扫描子目录。模型在 [Modules/ModuleStore.cs](Modules/ModuleStore.cs)`ModuleDefinition`/`ModuleMatch`/`ModuleRule`/`ModuleUnit`/`ModuleCountField`/`ModuleValueAdjustment`)。`RecommendedTalent``ModuleDefinition` 上的纯展示字段,不参与匹配(`ModuleMatch.Specificity` 不计入)。
- `ModuleStore``Reload`/`Save`/`Delete` 会在同一个门锁内串行完整文件事务与内存快照更新;`Save` 通过同目录临时文件提交,重命名失败会回滚新文件。编辑器写入不要绕过它,避免运行时读到半次操作。运行时工厂不再自行 `Reload`:启动和模块刷新先由 `ModuleDependencyService` 导入依赖、拒绝宏容量超限模块,再把已验证的内存快照交给运行时。
- 职业/专精明确的模块保存时会写入 `Dependencies` 快照。`ModuleDependencyService` 以本地为优先追加缺失的 ClassBlocks/spellsList/ClassMacros按动态宏 30 槽、其它宏 1 槽检查所有受影响专精;任一专精超过 keymap 容量就拒绝整个模块且不写 Lua。依赖提交失败会恢复配置和宏原文。
- 选择优先级:`ModuleStore.FindSelectedOrBestMatch` —— 先用 UI/参数选定的 `ModuleId`;否则取 `Match` 命中字段最多者(`ModuleMatch.Specificity` 越大越优先),并列按名称。`Match` 字段留空 = 任意。`PartyType` 数字会归一化为 `"1-40"`

View File

@@ -3,11 +3,14 @@ using System.Reflection;
namespace Shigure;
/// <summary>
/// 应用版本信息的单一来源。优先取 csproj 的 &lt;Version&gt;(AssemblyInformationalVersion),
/// 退化到 AssemblyVersion。
/// 应用名称与版本信息的单一来源AppName 取自程序集名,
/// Version 优先取 csproj 的 &lt;Version&gt;(AssemblyInformationalVersion)退化到 AssemblyVersion。
/// </summary>
internal static class AppInfo
{
// 取自程序集名(重命名程序集时自动跟随),与 Version 从程序集读取的惯例一致。
public static string AppName { get; } = Assembly.GetExecutingAssembly().GetName().Name ?? "Shigure";
public static string Version { get; } = ResolveVersion();
private static string ResolveVersion()

View File

@@ -289,15 +289,13 @@ public sealed class ModuleStore
public string ModuleDirectory { get; }
public static string ResolveModuleDirectory(string baseDirectory)
public static string ResolveModuleDirectory()
{
var currentModuleDirectory = Path.Combine(baseDirectory, "module");
if (Directory.Exists(currentModuleDirectory) || File.Exists(Path.Combine(baseDirectory, "Shigure.csproj")))
{
return currentModuleDirectory;
}
return Path.Combine(baseDirectory, "module");
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config",
AppInfo.AppName,
"module");
}
public IReadOnlyList<ModuleDefinition> GetModules()

View File

@@ -89,7 +89,7 @@ Program.Main
| 状态步骤映射 | ClassBlocks Lua经转换器生成 | `ConfigService` / `StateBuilder` | `config/*.json` |
| 法术宏到键位 | ClassMacros Lua经转换器生成 | `KeymapService` | `keymap/*.json` |
| 游戏插件副本 | 项目内置 `Fuyutsui/` | WoW AddOn 加载器 | `Interface/AddOns/Fuyutsui` |
| 行为规则 | 模块编辑器或手工 JSON | `ModuleStore` / `ModuleLogic` | `module/*.json` |
| 行为规则 | 模块编辑器或手工 JSON | `ModuleStore` / `ModuleLogic` | `~/.config/Shigure/module` |
| UI 缓存与偏好 | WinForms 界面 | WinForms 界面 | `cache/` |
| 运行状态 | `ShigureRuntime` 单循环 | `StatusForm` | `RenderSnapshot`,不持久化 |

View File

@@ -73,7 +73,7 @@ verified_at: 2026-08-10
## 核心数据与不变量
- 随机子进程**不复制业务数据目录**`config``keymap``module``cache` 仍由原始根目录提供。
- 随机子进程**不复制业务数据目录**`config``keymap``cache` 仍由原始根目录提供,模块由用户配置目录 `~/.config/Shigure/module` 提供。
- `SHIGURE_RANDOMIZED_PROCESS=1` 是防递归开关。外部若伪造相关环境变量,也会改变启动和路径行为。
- 清理范围是按源 EXE 路径哈希隔离的专属临时根目录;清理会递归删除其所有子项并吞掉 IO/权限异常。
- 每次会话构造新的 `ConfigService``KeymapService``PixelScanner``StateBuilder``KeySender``LogicRegistry``ModuleStore` 与触发键读取器跨会话共享。

View File

@@ -56,7 +56,7 @@ verified_at: 2026-08-10
| `MainForm` 浮条 | 启停、打开设置/状态、关闭、拖动 | 会话状态、UI 缓存 | `SetEnabled`、重启/停止、窗口偏好 |
| 通用设置 | 触发键、模式、模块选择、配置生成与插件部署 | `AppOptions`、模块目录、项目 Fuyutsui | 缓存/设置、会话重启、同步队列 |
| `StatusForm` | 九个状态/诊断页 | `RenderSnapshot`、日志 | 不拥有运行状态 |
| 模块编辑器 | 匹配、规则、动态单位、数量、调整/公式、排序 | 模块快照 | `module/*.json` |
| 模块编辑器 | 匹配、规则、动态单位、数量、调整/公式、排序 | 模块快照 | `~/.config/Shigure/module` |
| ClassBlocks 编辑器 | 职业/专精状态块编辑 | `Fuyutsui/class/*.lua` 项目源 | ClassBlocks table、config、当前 Lua 游戏部署 |
| ClassMacros 编辑器 | common/spec/static/special 宏编辑 | `Fuyutsui/core/classmacros.lua` 项目源 | ClassMacros table、keymap、当前 Lua 游戏部署 |
| 条件/单位对话框 | 构造受限条件和动态定义 | 当前草稿 | 返回编辑器内存模型 |

View File

@@ -41,7 +41,7 @@ verified_at: 2026-08-10
# Shigure 本地数据、路径、构建与验证
> [!abstract] AI 快速摘要
> Shigure 的业务根目录通常是正式 EXE 所在目录;随机副本子进程通过环境变量仍指回该目录。程序从这里读取内置 `Fuyutsui/`、`config`、`keymap`、`module`、`cache` 和 `wow_process.txt`。项目插件是权威源,目标游戏中的 Fuyutsui 只是由部署服务维护的运行副本。项目是无第三方 NuGet 引用的 `net10.0-windows` WinForms WinExe仓库没有测试项目。
> Shigure 的业务根目录通常是正式 EXE 所在目录;随机副本子进程通过环境变量仍指回该目录。程序从这里读取内置 `Fuyutsui/`、`config`、`keymap`、`cache` 和 `wow_process.txt`;模块从用户配置目录 `~/.config/Shigure/module` 读取。项目插件是权威源,目标游戏中的 Fuyutsui 只是由部署服务维护的运行副本。项目是无第三方 NuGet 引用的 `net10.0-windows` WinForms WinExe仓库没有测试项目。
## 图谱位置
@@ -65,7 +65,7 @@ verified_at: 2026-08-10
AppContext.BaseDirectory = 随机临时目录(仅运行依赖)
```
这使随机 EXE 可以加载临时目录 DLL同时配置和用户数据仍落在正式安装目录。环境变量属于信任边界:若外部预先设置/篡改,可能改变业务数据根目录。
这使随机 EXE 可以加载临时目录 DLL同时配置、Keymap 和缓存等数据仍落在正式安装目录;模块则在用户配置目录(子进程与父进程同属一个 Windows 用户,解析结果一致)。环境变量属于信任边界:若外部预先设置/篡改,可能改变业务数据根目录。
## 本地数据地图
@@ -74,7 +74,7 @@ verified_at: 2026-08-10
| `config/common.json` | 协议固定字段 | `ConfigService` | 打包内容/人工维护 |
| `config/<class>.json` | 13 职业和专精步骤映射、法术元数据 | `ConfigService`, `StateBuilder` | `FuyutsuiConfigConverter` |
| `keymap/*.json` | unit/spell/macroCondition→hotkey | `KeymapService`, `KeymapCatalog` | `FuyutsuiKeymapConverter` |
| `module/**/*.json` | 匹配、规则、动态值和公式 | `ModuleStore` | 模块编辑器/人工 |
| `~/.config/Shigure/module/**/*.json` | 匹配、规则、动态值和公式 | `ModuleStore` | 模块编辑器/人工 |
| `cache/` | 窗口和 UI 偏好 | UI 基础设施 | MainForm/各 UI |
| `Fuyutsui/class/*.lua` | 内置 ClassBlocks 权威源 | ClassBlocks Store/转换器 | ClassBlocks 编辑器/人工 |
| `Fuyutsui/core/classmacros.lua` | 内置宏权威源 | ClassMacros Store/转换器 | 宏编辑器/人工 |
@@ -99,7 +99,7 @@ verified_at: 2026-08-10
- 开启 nullable 与 implicit usings。
- 应用版本在项目文件中为 1.2.1,并被模块编辑器保存到模块元数据。
- 项目没有 `PackageReference`;核心只依赖 .NET/WinForms/System.Drawing 和 Win32 P/Invoke。
- 项目文件声明嵌入 UI assets并把 `Fuyutsui/**`、配置、Keymap、模块`wow_process.txt` 按规则复制到输出Fuyutsui 同时明确复制到 publish。
- 项目文件声明嵌入 UI assets并把 `Fuyutsui/**`、配置、Keymap `wow_process.txt` 按规则复制到输出Fuyutsui 同时明确复制到 publish。模块不随构建/发布复制,由用户配置目录 `~/.config/Shigure/module` 提供。
- 实际打包策略还应对照 `打包说明.md`;随机启动器只复制正式输出目录**顶层**运行依赖到临时目录。
## 当前验证能力

View File

@@ -53,7 +53,7 @@ Shigure 是一个 Windows WinForms 桌面程序。它从目标窗口读取 Fuyut
- `日志`:记录启动、停止、职业识别、模块匹配、逻辑状态、施放步骤、配置同步和异常。
- `关于`:显示产品、公司、版本、模块目录和配置目录。
触发键、发送模式、模块选择、模块保存以及配置同步都会按需重启运行循环。窗口位置、大小、模块选择和表格列宽等 UI 状态保存在 `cache/window-state.json``module/``cache/` 都是本地数据,默认不提交到 Git。
触发键、发送模式、模块选择、模块保存以及配置同步都会按需重启运行循环。窗口位置、大小、模块选择和表格列宽等 UI 状态保存在 `cache/window-state.json`;模块保存在用户配置目录 `~/.config/Shigure/module``cache/` 与模块数据都是本地数据,默认不提交到 Git。
## 环境要求
@@ -87,7 +87,7 @@ dotnet run --project .\Shigure.csproj -- --toggle XBUTTON2 --mode switch --logic
- `click`:每次按下只执行一轮逻辑。
- `hold`:按住时运行,松开后停止。
程序会直接从当前 EXE 所在目录运行,并从该目录读取 `Fuyutsui/``config/``keymap/``module/``wow_process.txt`
程序会直接从当前 EXE 所在目录运行,并从该目录读取 `Fuyutsui/``config/``keymap/``wow_process.txt`;模块从用户配置目录 `~/.config/Shigure/module` 读取
## 构建
@@ -95,7 +95,7 @@ dotnet run --project .\Shigure.csproj -- --toggle XBUTTON2 --mode switch --logic
dotnet build .\Shigure.csproj
```
应用图标为 `Assets\arasaka-icon.ico`。项目会把 `Fuyutsui/**``config/*.json``keymap/*.json`、本地 `module/**/*.json``wow_process.txt` 复制到输出/发布目录;图标、职业图和专精图作为嵌入资源打包。
应用图标为 `Assets\arasaka-icon.ico`。项目会把 `Fuyutsui/**``config/*.json``keymap/*.json``wow_process.txt` 复制到输出/发布目录;图标、职业图和专精图作为嵌入资源打包。
## 项目结构
@@ -111,7 +111,7 @@ Assets\ 应用图标、品牌资源、职业图和专精图
Fuyutsui\ 权威插件源码、配置/宏编辑源及游戏部署源
config\ 由 Fuyutsui 职业配置生成的扫描映射
keymap\ 由 Fuyutsui 职业宏生成的按键映射
module\ 本地模块(运行后或在 UI 中保存时创建
module\ 模块示例模板(运行时模块保存在用户配置目录 `~/.config/Shigure/module`
Tools\ 辅助脚本
```
@@ -159,9 +159,9 @@ Tools\ 辅助脚本
## 模块系统
模块以 `模块名.json` 保存在 `module/`。名称不能重复;加载时会递归扫描子目录,以兼容旧版布局。模块页保存时会写入当前 Shigure 版本。职业和专精均已指定时,模块还会携带该专精的 `ClassBlocks`、职业 `spellsList`,以及该职业的通用/专精动态宏、静态宏和特殊宏;旧模块没有这些依赖信息时仍可正常使用。
模块以 `模块名.json` 保存在用户配置目录 `~/.config/Shigure/module`。名称不能重复;加载时会递归扫描子目录,以兼容旧版布局。模块页保存时会写入当前 Shigure 版本。职业和专精均已指定时,模块还会携带该专精的 `ClassBlocks`、职业 `spellsList`,以及该职业的通用/专精动态宏、静态宏和特殊宏;旧模块没有这些依赖信息时仍可正常使用。
启动和“刷新模块”会把模块携带而本地缺少的配置与宏追加到项目 `Fuyutsui/`,不会覆盖或删除本地已有条目。发生新增后会自动重建 `config/keymap`、同步游戏插件并按需重启运行。导入前会按每项动态宏 30 个槽位、静态/特殊宏各 1 个槽位检查该职业所有受影响专精;合并结果超过 273 个槽位时,整个模块不会进入模块列表或运行时,本地 Lua 也不会被修改。模块文件仍保留在 `module/`,清理宏后可刷新重试。
启动和“刷新模块”会把模块携带而本地缺少的配置与宏追加到项目 `Fuyutsui/`,不会覆盖或删除本地已有条目。发生新增后会自动重建 `config/keymap`、同步游戏插件并按需重启运行。导入前会按每项动态宏 30 个槽位、静态/特殊宏各 1 个槽位检查该职业所有受影响专精;合并结果超过 273 个槽位时,整个模块不会进入模块列表或运行时,本地 Lua 也不会被修改。模块文件仍保留在模块目录,清理宏后可刷新重试。
模块匹配字段:

View File

@@ -30,7 +30,6 @@
<EmbeddedResource Include="Assets\Spec\*.jpg" />
<None Include="config\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="keymap\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="module\**\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="wow_process.txt" CopyToOutputDirectory="PreserveNewest" />
<None Update="Fuyutsui\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

View File

@@ -3031,7 +3031,7 @@ public sealed class ModuleEditorControl : UserControl
private void OpenModuleFolder()
{
var moduleDirectory = ModuleStore.ResolveModuleDirectory(_baseDirectory);
var moduleDirectory = ModuleStore.ResolveModuleDirectory();
var filePath = _selectedModule?.FilePath;
try
{

View File

@@ -737,7 +737,7 @@ public sealed class StatusForm : Form
AddAboutRow(details, "类型", "冲锋枪");
AddAboutRow(details, "介绍", "它一分钟打出去的子弹比荒坂偷的税还要多。");
AddAboutRow(details, "用途", "有时人们只想把子弹全打出去,在硝烟过后品味眼前的一片狼藉。");
var modulePath = ModuleStore.ResolveModuleDirectory(AppPaths.BaseDirectory);
var modulePath = ModuleStore.ResolveModuleDirectory();
var configPath = ConfigService.ResolveConfigPath(AppPaths.BaseDirectory);
AddAboutRow(details, "模块目录", FormatAboutPath(modulePath), modulePath);
AddAboutRow(details, "配置目录", FormatAboutPath(configPath), configPath);
@@ -862,6 +862,13 @@ public sealed class StatusForm : Form
var baseDirectory = Path.GetFullPath(AppPaths.BaseDirectory);
var fullPath = Path.GetFullPath(path);
var relativePath = Path.GetRelativePath(baseDirectory, fullPath);
// 路径位于 exe 目录之外时直接显示完整路径,避免冗长的 ..\..\ 相对路径文本。
if (relativePath == ".."
|| relativePath.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal))
{
return fullPath;
}
return string.IsNullOrWhiteSpace(relativePath) ? "." : relativePath;
}
catch

View File

@@ -138,12 +138,13 @@ artifacts\publish\win-x64
Fuyutsui
config
keymap
module
wow_process.txt
```
其中插件目录是插件权威源,程序会在启动或“更新配置”时把它部署到已运行游戏的 `Interface\AddOns\插件名称``Assets` 中的图标和职业/专精图片已作为资源嵌入程序,不要求单独分发。上述运行数据会在发布时复制到输出目录,最稳妥的方式仍是把 `win-x64` 整个文件夹压缩成 zip 再发送。
模块不随发布包分发:模块保存在用户配置目录 `~/.config/Shigure/module`(首次运行自动创建),全新用户首次启动模块列表为空。使用改名脚本打包时该目录随程序名变化,例如改名为 `MyShigure` 后为 `~/.config/MyShigure/module`
不要只发送单个 exe也不要删除发布目录中的插件目录。如果缺少内置插件目录配置/宏编辑和游戏插件部署都会失败。
## 7. 常见问题