|
| 1 | +# PCL.Neo 贡献指南与开发规范 |
| 2 | + |
| 3 | +## 项目概述 |
| 4 | + |
| 5 | +PCL.Neo 是一个基于 .NET 9 和 Avalonia 框架开发的 Minecraft 启动器项目。项目采用分层架构设计,包含以下主要模块: |
| 6 | + |
| 7 | +- **PCL.Neo**: UI 层,基于 Avalonia 框架的用户界面 |
| 8 | +- **PCL.Neo.Core**: 核心业务逻辑层,包含游戏启动、版本管理等核心功能 |
| 9 | +- **PCL.Neo.Tests**: 单元测试项目,确保代码质量和稳定性 |
| 10 | + |
| 11 | +## 开发环境要求 |
| 12 | + |
| 13 | +### 系统要求 |
| 14 | +- .NET 9 SDK 或更高版本 |
| 15 | +- Visual Studio 2022 或 JetBrains Rider 或 VS Code |
| 16 | +- Git 版本控制工具 |
| 17 | + |
| 18 | +### 推荐工具 |
| 19 | +- GitHub Desktop 或其他 Git 客户端 |
| 20 | +- NuGet Package Manager |
| 21 | +- 代码分析工具(如 SonarLint) |
| 22 | + |
| 23 | +## 构建指令 |
| 24 | + |
| 25 | +### 命令行构建 |
| 26 | + |
| 27 | +项目支持通过命令行进行构建,以下是标准的构建命令: |
| 28 | + |
| 29 | +#### Debug 构建 |
| 30 | +```bash |
| 31 | +# 恢复 NuGet 包 |
| 32 | +dotnet restore |
| 33 | + |
| 34 | +# Debug 构建(单文件发布,关闭代码裁剪) |
| 35 | +dotnet publish PCL.Neo/PCL.Neo.csproj -c Debug -r win-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false -o ./publish/debug |
| 36 | + |
| 37 | +# 运行测试 |
| 38 | +dotnet test PCL.Neo.Tests/ |
| 39 | +``` |
| 40 | + |
| 41 | +#### Release 构建 |
| 42 | +```bash |
| 43 | +# 恢复 NuGet 包 |
| 44 | +dotnet restore |
| 45 | + |
| 46 | +# Release 构建(单文件发布,关闭代码裁剪) |
| 47 | +dotnet publish PCL.Neo/PCL.Neo.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false -o ./publish/release |
| 48 | + |
| 49 | +# 运行测试 |
| 50 | +dotnet test PCL.Neo.Tests/ -c Release |
| 51 | +``` |
| 52 | + |
| 53 | +#### 跨平台构建 |
| 54 | +```bash |
| 55 | +# Windows x64 |
| 56 | +dotnet publish PCL.Neo/PCL.Neo.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false -o ./publish/win-x64 |
| 57 | + |
| 58 | +# Linux x64 |
| 59 | +dotnet publish PCL.Neo/PCL.Neo.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false -o ./publish/linux-x64 |
| 60 | + |
| 61 | +# macOS x64 |
| 62 | +dotnet publish PCL.Neo/PCL.Neo.csproj -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false -o ./publish/osx-x64 |
| 63 | +``` |
| 64 | + |
| 65 | +### 构建要求 |
| 66 | +- **单文件发布**: 使用 `PublishSingleFile=true` 将应用程序打包为单个可执行文件 |
| 67 | +- **关闭代码裁剪**: 使用 `PublishTrimmed=false` 确保所有依赖项都包含在内,避免运行时缺少程序集 |
| 68 | +- **自包含部署**: 使用 `--self-contained true` 包含 .NET 运行时,无需目标机器安装 .NET |
| 69 | + |
| 70 | +### 验证构建 |
| 71 | +构建完成后,请验证: |
| 72 | +- [ ] 可执行文件能够正常启动 |
| 73 | +- [ ] 所有功能正常工作 |
| 74 | +- [ ] 没有缺少依赖项的错误 |
| 75 | +- [ ] 单元测试全部通过 |
| 76 | + |
| 77 | +## 项目结构 |
| 78 | + |
| 79 | +``` |
| 80 | +PCL.Neo/ |
| 81 | +├── PCL.Neo/ # UI 层 - Avalonia 界面项目 |
| 82 | +├── PCL.Neo.Core/ # 核心层 - 业务逻辑和数据处理 |
| 83 | +│ ├── Models/ # 数据模型 |
| 84 | +│ ├── Services/ # 业务服务 |
| 85 | +│ ├── Utils/ # 工具类 |
| 86 | +│ └── ... |
| 87 | +├── PCL.Neo.Tests/ # 测试项目 |
| 88 | +├── docs/ # 文档 |
| 89 | +└── README.md |
| 90 | +``` |
| 91 | + |
| 92 | +## 代码规范 |
| 93 | + |
| 94 | +### 命名约定 |
| 95 | + |
| 96 | +遵循 Microsoft C# 命名约定: |
| 97 | + |
| 98 | +- **类名**: PascalCase(如 `GameLauncher`) |
| 99 | +- **方法名**: PascalCase(如 `LaunchAsync`) |
| 100 | +- **属性名**: PascalCase(如 `RootDirectory`) |
| 101 | +- **字段名**: camelCase,私有字段使用下划线前缀(如 `_isIndie`) |
| 102 | +- **常量**: PascalCase(如 `DefaultTimeout`) |
| 103 | +- **接口**: 以 `I` 开头的 PascalCase(如 `IGameLauncher`) |
| 104 | + |
| 105 | +### 代码风格 |
| 106 | + |
| 107 | +项目使用 EditorConfig 统一代码风格,主要规范包括: |
| 108 | + |
| 109 | +- **缩进**: 4 个空格,不使用制表符 |
| 110 | +- **换行**: 使用 CRLF(Windows 风格) |
| 111 | +- **括号**: 所有控制结构都使用大括号,大括号另起一行 |
| 112 | +- **空格**: 在操作符前后添加空格,逗号后添加空格 |
| 113 | +- **文件编码**: UTF-8 |
| 114 | + |
| 115 | +### 代码质量要求 |
| 116 | + |
| 117 | +#### 1. 异常处理 |
| 118 | +如果需要忽略异常请使用`try-catch`块捕获后在需要忽略的部分使用注释说明 |
| 119 | +不要随意捕获异常,没有必要的捕获请删除 |
| 120 | +如果需要日志记录请使用`Utils`中的`Logger`,并将异常也传入`Logger`的参数中 |
| 121 | +```csharp |
| 122 | +// 好的示例 |
| 123 | +public async Task<Process> LaunchAsync(GameProfile profile) |
| 124 | +{ |
| 125 | + if (profile == null) |
| 126 | + throw new ArgumentNullException(nameof(profile)); |
| 127 | + |
| 128 | + string mcDir = profile.Information.RootDirectory; |
| 129 | + if (!Directory.Exists(mcDir)) |
| 130 | + { |
| 131 | + throw new DirectoryNotFoundException($"Minecraft root directory not found. {mcDir}"); |
| 132 | + } |
| 133 | + |
| 134 | + // ... 其他逻辑 |
| 135 | +} |
| 136 | + |
| 137 | +public int Foo() |
| 138 | +{ |
| 139 | + try |
| 140 | + { |
| 141 | + // do stuff... |
| 142 | + } |
| 143 | + catch (IOException ex) |
| 144 | + { |
| 145 | + Logger.Error("Catched exception.", ex); |
| 146 | + // do stuff... |
| 147 | + } |
| 148 | + catch (Exception) |
| 149 | + { |
| 150 | + // 忽略异常 |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +#### 2. 空值检查 |
| 156 | +将所有的有关空值的Warning视为Error,正确地处理空值,谨防`NullRefreenceEexception` |
| 157 | +```csharp |
| 158 | +// 使用 null 条件运算符和 ArgumentNullException.ThrowIfNull |
| 159 | +ArgumentNullException.ThrowIfNull(versionManifes.Libraries); |
| 160 | + |
| 161 | +// 使用 null 合并运算符 |
| 162 | +var assetIndex = versionManifes.AssetIndex?.Id ?? "legacy"; |
| 163 | +``` |
| 164 | + |
| 165 | +#### 3. 资源管理 |
| 166 | +应使用`IDisposable`进行资源释放,尽量减少`Finalizer`的使用 |
| 167 | +```csharp |
| 168 | +// 使用 using 语句确保资源释放 |
| 169 | +using var fileStream = new FileStream(path, FileMode.Open); |
| 170 | +// 或者 |
| 171 | +using (var process = new Process()) |
| 172 | +{ |
| 173 | + // 处理逻辑 |
| 174 | +} |
| 175 | +``` |
| 176 | + |
| 177 | +#### 4. 异步编程 |
| 178 | +```csharp |
| 179 | +// 正确使用 async/await |
| 180 | +public async Task<VersionManifes> GetVersionAsync(string versionId) |
| 181 | +{ |
| 182 | + var result = await SomeAsyncOperation(); |
| 183 | + return result; |
| 184 | +} |
| 185 | + |
| 186 | +// 避免异步方法的阻塞调用 |
| 187 | +// 错误: SomeAsyncMethod().Result |
| 188 | +// 正确: await SomeAsyncMethod() |
| 189 | +``` |
| 190 | + |
| 191 | +### 注释规范 |
| 192 | +对于注释请使用`//+空格+内容`的形式 |
| 193 | +#### 1. XML 文档注释 |
| 194 | +对于公开的方法请添加XML注释以便于后续使用,如果方法有抛出异常请使用`exception`块说明 |
| 195 | +```csharp |
| 196 | +/// <summary> |
| 197 | +/// 启动游戏 |
| 198 | +/// </summary> |
| 199 | +/// <param name="profile">游戏配置文件</param> |
| 200 | +/// <returns>游戏进程</returns> |
| 201 | +/// <exception cref="DirectoryNotFoundException">当游戏目录不存在时抛出</exception> |
| 202 | +public async Task<Process> LaunchAsync(GameProfile profile) |
| 203 | +{ |
| 204 | + // 实现逻辑 |
| 205 | +} |
| 206 | +``` |
| 207 | + |
| 208 | +#### 2. 行内注释 |
| 209 | +如果是为完成的部分请添加`// TODO`块,便于IDE智能查找 |
| 210 | +```csharp |
| 211 | +// 确保目录存在 |
| 212 | +if (!Directory.Exists(mcDir)) |
| 213 | +{ |
| 214 | + throw new DirectoryNotFoundException($"Minecraft root directory not found. {mcDir}"); |
| 215 | +} |
| 216 | + |
| 217 | +// TODO: 从配置文件加载版本信息 |
| 218 | +var launcherVersion = "1.0.0"; |
| 219 | +``` |
| 220 | + |
| 221 | +## 测试规范 |
| 222 | +测试使用NUnit 4.3.2框架 |
| 223 | +PCL.Neo.Core中的测试代码请放到Core文件夹中 |
| 224 | +### 单元测试要求 |
| 225 | + |
| 226 | +1. **测试覆盖率**: 核心业务逻辑测试覆盖率应达到 80% 以上 |
| 227 | +2. **测试命名**: 使用 `方法名_测试场景_期望结果` 格式 |
| 228 | +3. **测试结构**: 使用 AAA 模式(Arrange, Act, Assert) |
| 229 | + |
| 230 | +```csharp |
| 231 | +[Test] |
| 232 | +public async Task LaunchAsync_ValidProfile_ReturnsProcess() |
| 233 | +{ |
| 234 | + // Arrange |
| 235 | + var profile = new GameProfile |
| 236 | + { |
| 237 | + Information = new GameInfo |
| 238 | + { |
| 239 | + RootDirectory = @"C:\Test\.minecraft", |
| 240 | + GameDirectory = @"C:\Test\.minecraft" |
| 241 | + } |
| 242 | + }; |
| 243 | + var launcher = new GameLauncher(); |
| 244 | + |
| 245 | + // Act |
| 246 | + var result = await launcher.LaunchAsync(profile); |
| 247 | + |
| 248 | + // Assert |
| 249 | + Assert.IsNotNull(result); |
| 250 | + Assert.IsInstanceOf<Process>(result); |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +### 测试分类 |
| 255 | + |
| 256 | +- **单元测试**: 测试单个方法或类的功能 |
| 257 | +- **集成测试**: 测试多个组件之间的交互 |
| 258 | +- **UI 测试**: 测试用户界面功能(使用 Avalonia 测试框架) |
| 259 | + |
| 260 | +## Git 工作流程 |
| 261 | + |
| 262 | +### 分支策略 |
| 263 | + |
| 264 | +采用 Git Flow 分支模型: |
| 265 | + |
| 266 | +- **main**: 主分支,包含生产就绪的代码 |
| 267 | +- **develop**: 开发分支,包含下一个发布版本的功能 |
| 268 | +- **feature/**: 功能分支,用于开发新功能 |
| 269 | +- **hotfix/**: 热修复分支,用于紧急修复 |
| 270 | +- **release/**: 发布分支,用于准备新版本发布 |
| 271 | + |
| 272 | +### 提交规范 |
| 273 | + |
| 274 | +使用语义化提交信息格式: |
| 275 | + |
| 276 | +``` |
| 277 | +<类型>[可选的作用域]: <描述> |
| 278 | +
|
| 279 | +[可选的正文] |
| 280 | +
|
| 281 | +[可选的脚注] |
| 282 | +``` |
| 283 | + |
| 284 | +**类型说明**: |
| 285 | +- `feat`: 新功能 |
| 286 | +- `fix`: 修复 bug |
| 287 | +- `docs`: 文档更新 |
| 288 | +- `style`: 代码格式调整(不影响功能) |
| 289 | +- `refactor`: 代码重构 |
| 290 | +- `test`: 添加或修改测试 |
| 291 | +- `chore`: 构建过程或辅助工具的变动 |
| 292 | + |
| 293 | +**示例**: |
| 294 | +``` |
| 295 | +feat(launcher): 添加游戏启动前的版本检查功能 |
| 296 | +
|
| 297 | +- 添加版本兼容性检查 |
| 298 | +- 优化启动参数构建逻辑 |
| 299 | +- 增加错误处理和用户提示 |
| 300 | +
|
| 301 | +Closes #123 |
| 302 | +``` |
| 303 | + |
| 304 | +## Pull Request 规范 |
| 305 | + |
| 306 | +### PR 标题格式 |
| 307 | +``` |
| 308 | +[类型] 简短描述 |
| 309 | +``` |
| 310 | + |
| 311 | +### PR 描述模板 |
| 312 | + |
| 313 | +```markdown |
| 314 | +# 前言 |
| 315 | +简要描述这个 PR 的目的和背景。 |
| 316 | + |
| 317 | +# 引入的 NuGet 包 |
| 318 | +如果引入了新的 NuGet 包,请列出: |
| 319 | +- 包名:简要说明用途 |
| 320 | +- 包名:简要说明用途 |
| 321 | + |
| 322 | +# 更改内容 |
| 323 | +详细列出主要更改: |
| 324 | +- 添加了 XXX 功能 |
| 325 | +- 修复了 XXX 问题 |
| 326 | +- 重构了 XXX 模块 |
| 327 | +- 优化了 XXX 性能 |
| 328 | + |
| 329 | +# 使用说明 |
| 330 | +如果涉及 API 更改或新功能,请提供使用示例: |
| 331 | + |
| 332 | +```csharp |
| 333 | +// 示例代码 |
| 334 | +var launcher = new GameLauncher(); |
| 335 | +var process = await launcher.LaunchAsync(profile); |
| 336 | +``` |
| 337 | + |
| 338 | +# 测试 |
| 339 | +说明如何测试这些更改: |
| 340 | +- [ ] 单元测试已通过 |
| 341 | +- [ ] 集成测试已通过 |
| 342 | +- [ ] 手动测试已完成 |
| 343 | + |
| 344 | +# 破坏性更改 |
| 345 | +如果有破坏性更改,请详细说明: |
| 346 | +- 更改的 API |
| 347 | +- 迁移指南 |
| 348 | +- 影响范围 |
| 349 | + |
| 350 | +# 相关 Issue |
| 351 | +关闭或关联的 Issue: |
| 352 | +- Closes #123 |
| 353 | +- Relates to #456 |
| 354 | + |
| 355 | +``` |
| 356 | +最后,感谢你对 PCL.Neo 项目的贡献!欢迎多多提交PR! |
0 commit comments