diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index a1929a8..6e9fa47 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -48,18 +48,36 @@ jobs: run: | mkdir -p config echo '[]' > config/mods_data.json + # Copy mod_rules.json if exists + if [ -f "config/mod_rules.json" ]; then + cp config/mod_rules.json config/mod_rules.json.bak + fi shell: bash - name: Build executable (Windows) if: matrix.os == 'windows' run: | - pyinstaller --clean Minecraft-mod-classifier.spec + pyinstaller --clean --noconfirm ` + --name "Minecraft-mod-classifier" ` + --onedir ` + --console ` + --paths src/python ` + --add-data "config/mods_data.json;config" ` + --add-data "config/mod_rules.json;config" ` + src/python/main.py shell: pwsh - name: Build executable (Linux) if: matrix.os == 'linux' run: | - pyinstaller --clean Minecraft-mod-classifier.spec + pyinstaller --clean --noconfirm \ + --name "Minecraft-mod-classifier" \ + --onedir \ + --console \ + --paths src/python \ + --add-data "config/mods_data.json:config" \ + --add-data "config/mod_rules.json:config" \ + src/python/main.py - name: Prepare release package (Windows) if: matrix.os == 'windows' @@ -69,8 +87,8 @@ jobs: Copy-Item "README.md" "release\Minecraft-mod-classifier\" Copy-Item "LICENSE" "release\Minecraft-mod-classifier\" Copy-Item "docs\QUICKSTART.md" "release\Minecraft-mod-classifier\" - Copy-Item "docs\USAGE.md" "release\Minecraft-mod-classifier\" Copy-Item "docs\BUILD_GUIDE.md" "release\Minecraft-mod-classifier\" + Copy-Item "GITHUB_INTEGRATION.md" "release\Minecraft-mod-classifier\" Remove-Item "release\Minecraft-mod-classifier\Input\*" -Recurse -ErrorAction SilentlyContinue Remove-Item "release\Minecraft-mod-classifier\Output\*" -Recurse -ErrorAction SilentlyContinue shell: pwsh @@ -83,8 +101,8 @@ jobs: cp README.md release/Minecraft-mod-classifier/ cp LICENSE release/Minecraft-mod-classifier/ cp docs/QUICKSTART.md release/Minecraft-mod-classifier/ - cp docs/USAGE.md release/Minecraft-mod-classifier/ cp docs/BUILD_GUIDE.md release/Minecraft-mod-classifier/ + cp GITHUB_INTEGRATION.md release/Minecraft-mod-classifier/ rm -rf release/Minecraft-mod-classifier/Input/* rm -rf release/Minecraft-mod-classifier/Output/* chmod +x release/Minecraft-mod-classifier/Minecraft-mod-classifier diff --git a/.gitignore b/.gitignore index 3604a89..c2b6f38 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,6 @@ mod_classifier.log # 配置文件(包含用户数据,不应版本控制) config/mods_data.json -config/mods_data.json.backup config/settings.json # 输入输出目录(用户数据) diff --git a/GITHUB_INTEGRATION.md b/GITHUB_INTEGRATION.md new file mode 100644 index 0000000..409014c --- /dev/null +++ b/GITHUB_INTEGRATION.md @@ -0,0 +1,314 @@ +# GitHub集成功能使用说明 + +## 功能概述 + +Minecraft Mod Classifier 现在支持智能规则更新和自动生成补丁文件,帮助社区共享Mod分类数据。 + +**主要功能:** +- 🔄 **批量规则更新** - 基于在线数据源批量更新分类规则 +- 📊 **差异分级处理** - 自动识别重大差异和轻微差异,分别处理 +- 🔒 **智能锁死机制** - 重大差异自动添加确认锁,轻微差异保持可修改 +- 📝 **增量补丁生成** - 自动生成规则更新补丁,便于审查和合并 + +## 使用流程 + +### 1. 批量规则更新(新增) + +基于在线数据源批量更新分类规则: + +```bash +# 运行批量更新脚本 +python scripts/apply_online_rules.py + +# 程序会自动: +# - 比对本地配置与在线数据 +# - 识别重大差异(如 client_only vs server_only) +# - 识别轻微差异(四个可选类型之间) +# - 应用在线数据的分类 +# - 重大差异添加锁死,轻微差异保持可修改 +``` + +**差异分级:** +- **重大差异**:完全不同的类型 → 自动添加 `confirmed: true` 锁死 +- **轻微差异**:四个可选类型之间 → 不锁死,仅更新 reason + +### 2. 自动生成补丁 + +当分类完成并检测到新 Mod 时,程序会自动生成规则更新补丁: + +``` +[2026-04-30 17:52:23] INFO: 正在生成规则更新补丁... +✓ 成功生成增量补丁: rule_update_patch_20260430_175223.json + 新增规则: 0 条 + 更新规则: 72 条 + 总计变更: 72 条 + +📤 如何提交更新: + 方法1(推荐 - 使用合并工具): + python src/python/apply_patch.py rule_update_patch_20260430_175223.json + + 方法2(手动 - 通过GitHub Issue): + 1. 打开 GitHub Issues + 2. 创建新Issue,标题:规则数据库更新 - 2026-04-30 + 3. 将此JSON文件内容粘贴到Issue中 + 4. 维护者会使用工具自动合并 +``` + +### 3. 补丁文件结构 + +生成的补丁文件包含以下信息: + +```json +{ + "version": "2.0", + "generated_at": "2026-04-30T17:52:23.xxx", + "description": "Minecraft Mod Classifier 规则数据库增量更新补丁", + "summary": { + "total_changes": 72, + "new_rules": 0, + "updated_rules": 72 + }, + "new_rules": [], + "updated_rules": [ + { + "mod_id": "sodium", + "changes": { + "reason": { + "old": "", + "new": "" + } + } + } + ], + "merge_instructions": [...] +} +``` + +### 4. 工作流程 + +1. **分类完成** → 保存 mods_data.json(包含 reason 字段) +2. **生成补丁** → 比较 mods_data.json 和 mod_rules.json +3. **检测差异** → 生成增量补丁文件 +4. **同步规则** → 将配置更新至 mod_rules.json + +**关键点:** +- 补丁在规则同步**之前**生成,确保能检测到所有变更 +- mods_data.json 和 mod_rules.json 都包含 reason 字段(默认为空字符串) +- 只有当检测到新 Mod 时才会生成补丁 + +## 前提条件 + +### 必须满足的条件 + +1. **Python 环境** + ```bash + python --version # 需要 Python 3.7+ + ``` + +2. **配置文件存在** + - `config/mods_data.json` - Mod 配置数据库 + - `config/mod_rules.json` - 规则数据库 + +### 推荐的准备工作 + +1. **安装依赖**(如需使用 apply_patch.py) + ```bash + pip install -r requirements.txt + ``` + +2. **了解 Git 基本操作**(可选,用于提交补丁) + +## 手动提交流程 + +如果需要使用补丁文件,可以手动执行: + +```bash +# 1. 查看补丁内容 +cat rule_update_patch_20260430_175223.json + +# 2. 使用合并工具应用补丁 +python src/python/apply_patch.py rule_update_patch_20260430_175223.json + +# 3. 检查合并结果 +git diff config/mod_rules.json + +# 4. 提交更改 +git add config/mod_rules.json +git commit -m "Update mod rules from patch" +git push +``` + +## 常见问题 + +### Q1: 没有生成补丁文件 + +**解决方案:** +- 确认是否有新 Mod 被检测到(`auto_detected > 0`) +- 检查 `config/mods_data.json` 是否为空 +- 查看日志中是否有“正在生成规则更新补丁...”的信息 + +### Q2: 补丁显示 0 条变更 + +**解决方案:** +- 这是正常情况,说明 mods_data.json 和 mod_rules.json 完全一致 +- 可能原因:所有 Mod 都已存在于规则数据库中 +- 或者 reason 字段已经同步 + +### Q3: 如何应用补丁 + +**解决方案:** +```bash +# 使用提供的合并工具 +python src/python/apply_patch.py <补丁文件名> + +# 或手动合并 +# 1. 打开补丁文件,复制 new_rules 和 updated_rules +# 2. 手动添加到 config/mod_rules.json +``` + +### Q4: 不想每次生成都提示 + +目前补丁生成是自动进行的(无需用户交互)。如需禁用,可以: + +1. 修改 `src/python/mod_classifier.py`,注释掉 `_generate_patch_before_sync()` 调用 +2. 或者在 `classify_mods()` 方法中移除相关代码 + +## 贡献指南 + +如果你希望为社区贡献规则更新: + +1. **运行分类程序** + - 将新的 Mod 放入 Input 目录 + - 运行程序进行分类 + +2. **获取补丁文件** + - 程序会自动生成 `rule_update_patch_*.json` + - 检查补丁内容是否正确 + +3. **提交到 GitHub** + - 方式1:使用 `apply_patch.py` 合并后提交 PR + - 方式2:将补丁文件内容粘贴到 GitHub Issue + +4. **等待审核** + - 维护者会审核补丁内容 + - 合并到主分支 + +## 技术实现 + +### 核心模块 + +- `src/python/generate_patch.py` - 补丁生成器 +- `src/python/apply_patch.py` - 补丁合并工具 +- `src/python/config_manager.py` - 配置管理器(含 reason 字段) +- `src/python/mod_classifier.py` - 分类器(在同步前生成补丁) + +### 关键方法 + +```python +# generate_patch.py +def generate_incremental_patch( + source_config: str = "config/mods_data.json", + target_rules: str = "config/mod_rules.json", + output_patch: str = None +) -> str: + """生成增量补丁文件""" + # 比较两个文件,找出差异 + # 返回补丁文件路径 + +# mod_classifier.py +def _generate_patch_before_sync(self): + """在同步规则之前生成增量补丁""" + # 此时 mods_data.json 已更新,但 mod_rules.json 还未同步 + # 可以正确检测到所有变更 +``` + +### 数据结构 + +**mods_data.json:** +```json +[ + { + "mod_id": "sodium", + "mod_name": "Sodium", + "type": "client_only", + "reason": "" // 新增字段,默认为空字符串 + } +] +``` + +**mod_rules.json:** +```json +{ + "rules": [ + { + "mod_id": "sodium", + "mod_name": "Sodium", + "type": "client_only", + "reason": "" // 已清理为空字符串 + } + ] +} +``` + +## 注意事项 + +⚠️ **重要提醒:** + +1. **数据一致性**: reason 字段在所有配置中都应存在(即使为空),确保比较时不会遗漏 +2. **补丁时机**: 补丁必须在规则同步之前生成,否则无法检测到变更 +3. **数据质量**: 请确保分类准确后再提交补丁,避免污染社区数据 +4. **频率限制**: 不要频繁生成大量补丁,建议批量处理后一次性提交 +5. **备份重要**: 在应用补丁前,建议备份 `config/mod_rules.json` + +## 示例输出 + +### 成功生成补丁 + +``` +[2026-04-30 17:52:23] INFO: 正在生成规则更新补丁... + +✓ 成功生成增量补丁: rule_update_patch_20260430_175223.json + 新增规则: 0 条 + 更新规则: 72 条 + 总计变更: 72 条 + +📤 如何提交更新: + 方法1(推荐 - 使用合并工具): + python src/python/apply_patch.py rule_update_patch_20260430_175223.json + + 方法2(手动 - 通过GitHub Issue): + 1. 打开 GitHub Issues + 2. 创建新Issue,标题:规则数据库更新 - 2026-04-30 + 3. 将此JSON文件内容粘贴到Issue中 + 4. 维护者会使用工具自动合并 +[2026-04-30 17:52:23] INFO: ✓ 增量补丁文件已生成: rule_update_patch_20260430_175223.json +``` + +### 无需生成补丁 + +``` +[2026-04-30 17:44:45] INFO: 规则数据库已是最新,无需生成补丁 +``` + +### 应用补丁 + +``` +$ python src/python/apply_patch.py rule_update_patch_20260430_175223.json +正在加载补丁文件: rule_update_patch_20260430_175223.json +✓ 补丁文件加载成功 + 新增规则: 0 条 + 更新规则: 72 条 +正在合并补丁... +✓ 成功合并 72 条规则更新 +✓ 规则数据库已更新 +``` + +--- + +**版本**: v0.1.6 +**最后更新**: 2026-04-30 +**主要变更**: +- 改为自动生成补丁(无需用户交互) +- 在规则同步之前生成补丁 +- 添加 reason 字段到所有配置 +- 清理 mod_rules.json 中的 reason 内容为空字符串 diff --git a/Minecraft-mod-classifier.spec b/Minecraft-mod-classifier.spec index 3fd7aca..643df24 100644 --- a/Minecraft-mod-classifier.spec +++ b/Minecraft-mod-classifier.spec @@ -1,18 +1,12 @@ # -*- mode: python ; coding: utf-8 -*- -import os -from pathlib import Path - -# 使用正斜杠确保跨平台兼容 -script_path = 'src/python/main.py' -config_data = ('config/mods_data.json', 'config') a = Analysis( - [script_path], + ['src\\python\\main.py'], pathex=['src/python'], binaries=[], - datas=[config_data], - hiddenimports=['mod_classifier', 'logger', 'config_manager', 'jar_parser', 'file_utils', 'i18n'], + datas=[('config/mods_data.json', 'config'), ('config/mod_rules.json', 'config')], + hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/README.md b/README.md index 5c9f266..042fba2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Python](https://img.shields.io/badge/Python-3.7+-blue.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]() -[![Version](https://img.shields.io/badge/Version-v0.1.7-orange.svg)](https://github.com/ZHwash/Minecraft-mod-classifier/releases/tag/v0.1.7) +[![Version](https://img.shields.io/badge/Version-v0.1.6-orange.svg)](https://github.com/ZHwash/Minecraft-mod-classifier/releases) > **⚠️ 注意:** 这是 [DHJComical/Minecraft-mod-classifier](https://github.com/DHJComical/Minecraft-mod-classifier) 的 Python 重构版本。原项目使用 C++ 实现,本版本完全重写为 Python,提供更简洁的代码、更好的跨平台支持和更低的贡献门槛。 @@ -12,11 +12,14 @@ 一个智能的 Minecraft Mod 分类工具,自动识别和分类 Mod 文件的运行端属性(客户端/服务端)。 **核心特性:** -- ✅ **智能双层分类** - 配置库快速匹配 + JAR 解析自动学习 +- ✅ **三层优先级分类** - JAR配置 > 规则数据库 > Modrinth API +- ✅ **自动学习机制** - 新Mod自动识别并同步至规则数据库 +- ✅ **智能规则更新** - 基于在线数据批量更新分类,支持差异分级处理 +- ✅ **增量补丁生成** - 自动生成规则更新补丁,便于审查和合并 - ✅ **多语言支持** - 中文/English 界面 - ✅ **全面格式支持** - Fabric/Forge/NeoForge - ✅ **零依赖运行** - 仅需 Python 标准库 -- ✅ **开箱即用** - 可打包为独立可执行文件 +- ✅ **小白友好贡献** - 自动生成补丁文件,无需Git知识 --- @@ -25,14 +28,14 @@ ### 方式一:使用独立可执行文件(推荐) #### Windows -1. 下载 [Releases](https://github.com/ZHwash/Minecraft-mod-classifier/releases) 中的 `minecraft-mod-classifier-v0.1.7-windows-x86_64.zip` +1. 下载 [Releases](https://github.com/ZHwash/Minecraft-mod-classifier/releases) 中的 `minecraft-mod-classifier-v0.1.6-windows-x86_64.zip` 2. 解压后双击 `Minecraft-mod-classifier.exe` 3. 将 `.jar` Mod 文件放入 `Input` 目录 4. 从 `Output` 目录获取分类结果 #### Linux/macOS ```bash -tar -xzf minecraft-mod-classifier-v0.1.7-linux-x86_64.tar.gz +tar -xzf minecraft-mod-classifier-v0.1.6-linux-x86_64.tar.gz cd Minecraft-mod-classifier chmod +x Minecraft-mod-classifier ./Minecraft-mod-classifier @@ -64,13 +67,6 @@ python src/python/main.py | ClientOptionalServerOptional | 两端都可选 | 配置库、API库 | | Unknown | 无法自动识别 | 需手动确认 | -### 智能文件名清洗 - -自动清理干扰信息: -``` -"[机械动力]create-1.21.1-6.0.10-neoforge.jar" → "create.jar" -"jei-1.16.5-7.7.1.118.jar" → "jei.jar" -``` ### 自动 JAR 解析 @@ -82,7 +78,41 @@ python src/python/main.py ### 自动学习机制 -首次运行时解析 JAR 并保存配置到 `config/mods_data.json`,后续运行直接查询,速度提升 **500倍**! +首次运行时解析 JAR 并保存配置到 `config/mods_data.json`,后续运行直接查询。每次运行后自动将新识别的Mod同步至 `config/mod_rules.json` 规则数据库。 + +### 🆕 GitHub 集成(新增) + +分类完成后,可选择生成规则更新补丁文件: + +```bash +# 分类完成后会自动生成补丁: +[2026-04-30 17:52:23] INFO: 正在生成规则更新补丁... +✓ 成功生成增量补丁: rule_update_patch_20260430_175223.json + 新增规则: 0 条 + 更新规则: 72 条 + 总计变更: 72 条 + +📤 如何提交更新: + 方法1(推荐 - 使用合并工具): + python src/python/apply_patch.py rule_update_patch_20260430_175223.json + + 方法2(手动 - 通过GitHub Issue): + 1. 打开 GitHub Issues: https://github.com/ZHwash/Minecraft-mod-classifier/issues + 2. 点击 'New Issue' + 3. 将补丁文件内容粘贴进去并提交 +``` + +**工作流程:** +1. 分类完成 → 保存 mods_data.json(包含 reason 字段) +2. 生成补丁 → 比较 mods_data.json 和 mod_rules.json +3. 检测到差异 → 生成增量补丁文件 +4. 同步规则 → 将配置更新至 mod_rules.json + +**前提条件:** +- 无需任何Git或GitHub Token配置 +- 生成的JSON补丁文件包含详细的提交说明 + +详细说明请查看 [GITHUB_INTEGRATION.md](GITHUB_INTEGRATION.md) --- @@ -114,21 +144,25 @@ Minecraft-mod-classifier/ │ ├── mod_classifier.py # 核心分类逻辑 │ ├── jar_parser.py # JAR包解析器 │ ├── config_manager.py # 配置管理器 +│ ├── rule_manager.py # 规则数据库管理 +│ ├── modrinth_api.py # Modrinth API集成 │ ├── file_utils.py # 文件工具函数 │ ├── logger.py # 日志系统 │ └── i18n.py # 国际化支持 ├── config/ # 配置文件 │ ├── mods_data.json # Mod配置数据库(自动生成) +│ ├── mod_rules.json # 规则数据库(830+条规则) │ └── settings.json # 用户设置 ├── Input/ # 输入目录(放入待分类Mod) ├── Output/ # 输出目录(分类结果) -├── docs/ # 文档 -│ ├── QUICKSTART.md # 快速入门 -│ ├── USAGE.md # 详细使用说明 -│ └── BUILD_GUIDE.md # 打包构建指南 -└── scripts/ # 实用脚本 - ├── run.bat/sh # 启动脚本 - └── build.bat/sh # 打包脚本 +│ ├── ClientOnly/ # 仅客户端 +│ ├── ServerOnly/ # 仅服务端 +│ ├── ClientAndServerRequired/ # 双端必需 +│ └── ... # 其他分类目录 +└── docs/ # 文档 + ├── QUICKSTART.md # 快速入门 + ├── USAGE.md # 详细使用说明 + └── BUILD_GUIDE.md # 打包构建指南 ``` --- @@ -156,12 +190,19 @@ Minecraft-mod-classifier/ ### 可以贡献的内容 -- 📝 **添加新的Mod分类规则** - 扩充 `mods_data.json` +- 📝 **添加新的Mod分类规则** - 通过生成补丁文件提交至GitHub Issues - 🐛 **修复Bug** - 提交Issue或直接PR - ✨ **添加新功能** - 如GUI界面、Web API等 - 📖 **改进文档** - 让新手更容易上手 - 🌍 **翻译支持** - 添加新语言 +**小白友好贡献流程:** +1. 运行分类程序,自动识别新Mod +2. 程序自动生成规则更新补丁文件 +3. 打开生成的JSON文件,复制内容 +4. 到GitHub Issues页面粘贴并提交 +5. 完成!无需任何Git知识 + 如果你想为**原 C++ 项目**贡献,请前往 [DHJComical/Minecraft-mod-classifier](https://github.com/DHJComical/Minecraft-mod-classifier)。 --- diff --git a/config/mod_rules.json b/config/mod_rules.json new file mode 100644 index 0000000..0416cf2 --- /dev/null +++ b/config/mod_rules.json @@ -0,0 +1,5186 @@ +{ + "rules": [ + { + "mod_id": "lithostitched", + "type": "server_only", + "reason": "", + "mod_name": "Lithostitched" + }, + { + "mod_id": "tectonic", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Tectonic" + }, + { + "mod_id": "jadeaddons", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "Jade Addons" + }, + { + "mod_id": "smsn", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Save My Shit Network" + }, + { + "mod_id": "dragonsurvival", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Dragon Survival" + }, + { + "mod_id": "integrated_api", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Integrated API" + }, + { + "mod_id": "integrated_stronghold", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Integrated Stronghold" + }, + { + "mod_id": "mechanicals", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Mechanicals Lib" + }, + { + "mod_id": "sable", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Sable" + }, + { + "mod_id": "idas", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Integrated Dungeons and Structures" + }, + { + "mod_id": "sophisticatedstorage", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Sophisticated Storage" + }, + { + "mod_id": "createbetterfps", + "type": "client_and_server_required", + "reason": "", + "mod_name": "CreateBetterFps" + }, + { + "mod_id": "dynamiccrosshair", + "type": "client_only", + "reason": "", + "mod_name": "Dynamic Crosshair" + }, + { + "mod_id": "spark", + "type": "client_optional_server_optional", + "reason": "通过JAR配置自动识别: spark", + "mod_name": "spark", + "confirmed": true + }, + { + "mod_id": "krypton", + "type": "server_only", + "reason": "", + "mod_name": "KryptonFoxified" + }, + { + "mod_id": "jei", + "type": "client_required_server_optional", + "reason": "通过JAR配置自动识别: Just Enough Items", + "mod_name": "Just Enough Items", + "confirmed": true + }, + { + "mod_id": "cme_championhelper", + "type": "unknown", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "timeslowmod", + "type": "unknown", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "hadenoughitems", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "jeroreintegration", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "potionparticlepack", + "type": "client_required_server_optional", + "reason": "通过JAR配置自动识别: Potion Particle Pack", + "mod_name": "Potion Particle Pack", + "confirmed": true + }, + { + "mod_id": "comics_bubbles_chat", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "xaerosworldmap", + "type": "client_required_server_optional", + "reason": "通过JAR配置自动识别", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "xaeros_minimap", + "type": "client_required_server_optional", + "reason": "通过JAR配置自动识别: Xaero's Minimap", + "mod_name": "Xaero's Minimap", + "confirmed": true + }, + { + "mod_id": "enhancedvisuals", + "type": "client_required_server_optional", + "reason": "通过JAR配置自动识别: EnhancedVisuals", + "mod_name": "EnhancedVisuals", + "confirmed": true + }, + { + "mod_id": "prism", + "type": "client_required_server_optional", + "reason": "通过JAR配置自动识别: Prism", + "mod_name": "Prism", + "confirmed": true + }, + { + "mod_id": "justenoughresources", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "konkrete", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Konkrete", + "confirmed": true + }, + { + "mod_id": "ingameinfoxml", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "forgeconfigscreens", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "loliasm", + "type": "client_required_server_optional", + "reason": "通过JAR配置自动识别: CensoredASM", + "mod_name": "CensoredASM", + "confirmed": true + }, + { + "mod_id": "iceberg", + "type": "client_only", + "reason": "通过JAR配置自动识别: Iceberg", + "mod_name": "Iceberg", + "confirmed": true + }, + { + "mod_id": "polylib", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "PolyLib", + "confirmed": true + }, + { + "mod_id": "astatine", + "type": "client_required_server_optional", + "reason": "根据网上资料修正(轻微差异): client_and_server_required → client_required_server_optional", + "mod_name": "" + }, + { + "mod_id": "distanthorizons", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Distant Horizons", + "confirmed": true + }, + { + "mod_id": "pretty_rain", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Pretty Rain", + "confirmed": true + }, + { + "mod_id": "sound_physics_remastered", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Sound Physics Remastered", + "confirmed": true + }, + { + "mod_id": "itemphysic", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "ItemPhysic", + "confirmed": true + }, + { + "mod_id": "lexiconfig", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Lexiconfig", + "confirmed": true + }, + { + "mod_id": "aquaacrobatics", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Aqua Acrobatics Legacy (ragecraft version)", + "confirmed": true + }, + { + "mod_id": "player_animation_lib", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "cloth_config", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Cloth Config v15 API", + "confirmed": true + }, + { + "mod_id": "kryptonreforged", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "configanytime", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "ConfigAnytime", + "confirmed": true + }, + { + "mod_id": "vintagefix", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "VintageFix" + }, + { + "mod_id": "cristellib", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Cristel Lib", + "confirmed": true + }, + { + "mod_id": "alfheim", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Alfheim", + "confirmed": true + }, + { + "mod_id": "flare", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "Flare (Spark for 1.12.2)" + }, + { + "mod_id": "common_networking", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "connectorextras", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "Connector Extras" + }, + { + "mod_id": "mixinbooter", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "MixinBooter", + "confirmed": true + }, + { + "mod_id": "fermiumbooter", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "FermiumBooter" + }, + { + "mod_id": "mixinbootstrap", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "MixinBootstrap" + }, + { + "mod_id": "fantasticlib", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "collective", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "Collective" + }, + { + "mod_id": "nightconfigfixes", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "rhino", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "Rhino" + }, + { + "mod_id": "openloader", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Open Loader", + "confirmed": true + }, + { + "mod_id": "fabric_api", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Forgified Fabric API" + }, + { + "mod_id": "recipeessentials", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "redirector", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Redirector", + "confirmed": true + }, + { + "mod_id": "redirectionor", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "saturn", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Saturn", + "confirmed": true + }, + { + "mod_id": "vanillaicecreamfix", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "VanillaIcecreamFix" + }, + { + "mod_id": "ksyxis", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Ksyxis", + "confirmed": true + }, + { + "mod_id": "modernfix", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "ModernFix", + "confirmed": true + }, + { + "mod_id": "nochatreports", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "No Chat Reports", + "confirmed": true + }, + { + "mod_id": "memorysweep", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "MemorySweep", + "confirmed": true + }, + { + "mod_id": "radium", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Radium", + "confirmed": true + }, + { + "mod_id": "midnightlib", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "MidnightLib" + }, + { + "mod_id": "ferritecore", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "FerriteCore", + "confirmed": true + }, + { + "mod_id": "modernui", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "ModernUI+", + "confirmed": true + }, + { + "mod_id": "smoothboot_reloaded", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "craftingtweaks", + "type": "client_optional_server_optional", + "reason": "", + "mod_name": "Crafting Tweaks" + }, + { + "mod_id": "smoothboot", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Smooth Boot", + "confirmed": true + }, + { + "mod_id": "achievementoptimizer", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "hybridfix", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "HybridFix" + }, + { + "mod_id": "unidict", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "UniDict", + "confirmed": true + }, + { + "mod_id": "noisium", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Noisium", + "confirmed": true + }, + { + "mod_id": "dimthread", + "type": "client_optional_server_required", + "reason": "根据网上资料修正(轻微差异): client_required_server_optional → client_optional_server_required", + "mod_name": "" + }, + { + "mod_id": "letmefeedyou", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mes", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "MES - Moog's End Structures", + "confirmed": true + }, + { + "mod_id": "healthnanfix", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "yungsapi", + "type": "client_optional_server_required", + "reason": "通过JAR配置自动识别: YUNG's API", + "mod_name": "YUNG's API", + "confirmed": true + }, + { + "mod_id": "yungsbridges", + "type": "client_optional_server_required", + "reason": "通过JAR配置自动识别: YUNG's Bridges", + "mod_name": "YUNG's Bridges", + "confirmed": true + }, + { + "mod_id": "towns_and_towers", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "tpmaster", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "tact", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "[TACZ] LesRaisins Tactical Equipements", + "confirmed": true + }, + { + "mod_id": "fastfurnace", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "FastFurnace [FABRIC]", + "confirmed": true + }, + { + "mod_id": "better_campfires", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Better Campfires", + "confirmed": true + }, + { + "mod_id": "alternate_current", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Alternate Current", + "confirmed": true + }, + { + "mod_id": "ftbquestsoptimizer", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ftbbackups2", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "starlight", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Starlight (Fabric)", + "confirmed": true + }, + { + "mod_id": "ati_structuresvanilla", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "aireducer", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "FPS Reducer", + "confirmed": true + }, + { + "mod_id": "rltweaker", + "type": "client_optional_server_required", + "reason": "根据网上资料修正(轻微差异): client_and_server_required → client_optional_server_required", + "mod_name": "" + }, + { + "mod_id": "born_in_a_barn", + "type": "client_optional_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "bilingualname", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "euphoriapatcher", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "loadingscreens", + "type": "client_only", + "reason": "", + "mod_name": "Loading Screen Tips" + }, + { + "mod_id": "bnbgaminglib", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "chunky", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Chunky", + "confirmed": true + }, + { + "mod_id": "incontrol", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "InControlMob", + "confirmed": true + }, + { + "mod_id": "journeymap", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Journeymap", + "confirmed": true + }, + { + "mod_id": "rrls", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "celeritas", + "type": "client_only", + "reason": "", + "mod_name": "Celeritas Dynamic Lights" + }, + { + "mod_id": "damagetilt", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "itlt", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "classicbar", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Classic Bad Omen", + "confirmed": true + }, + { + "mod_id": "armorsoundtweak", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "gamemodeswitcher1122", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mobends", + "type": "client_only", + "reason": "", + "mod_name": "Mo' Bends" + }, + { + "mod_id": "spartanhudbaubles", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "betterbiomeblend", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "torohealth", + "type": "client_only", + "reason": "", + "mod_name": "ToroHealth Damage Indicators (Updated)" + }, + { + "mod_id": "enablecheats_tow_edition1", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "bettertradingmenu", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "betterquestpopup", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "bettertitlescreen", + "type": "client_only", + "reason": "", + "mod_name": "Better Title Screen" + }, + { + "mod_id": "potiondescriptions", + "type": "client_only", + "reason": "", + "mod_name": "Potion Descriptions" + }, + { + "mod_id": "advanced_xray", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "continuity", + "type": "client_only", + "reason": "", + "mod_name": "Continuity" + }, + { + "mod_id": "inventoryhud", + "type": "client_only", + "reason": "", + "mod_name": "InventoryHUD+" + }, + { + "mod_id": "cullleaves", + "type": "client_only", + "reason": "", + "mod_name": "Cull Leaves" + }, + { + "mod_id": "notenoughanimations", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "NotEnoughAnimations", + "confirmed": true + }, + { + "mod_id": "searchables", + "type": "client_only", + "reason": "通过JAR配置自动识别: Searchables", + "mod_name": "Searchables", + "confirmed": true + }, + { + "mod_id": "colorfulhearts", + "type": "client_only", + "reason": "", + "mod_name": "Colorful Hearts" + }, + { + "mod_id": "crosshairbobbing", + "type": "client_only", + "reason": "", + "mod_name": "Crosshair Bobbing" + }, + { + "mod_id": "asyncparticles", + "type": "client_only", + "reason": "", + "mod_name": "AsyncParticles" + }, + { + "mod_id": "lazurite", + "type": "client_only", + "reason": "", + "mod_name": "Lazurite" + }, + { + "mod_id": "oculus", + "type": "client_only", + "reason": "", + "mod_name": "Oculus" + }, + { + "mod_id": "libipn", + "type": "client_only", + "reason": "", + "mod_name": "libIPN" + }, + { + "mod_id": "chloride", + "type": "client_only", + "reason": "", + "mod_name": "Chloride (Embeddium++/Sodium++)" + }, + { + "mod_id": "embeddium", + "type": "client_only", + "reason": "", + "mod_name": "Embeddium" + }, + { + "mod_id": "rubidium_extra", + "type": "client_only", + "reason": "", + "mod_name": "Embeddium (Rubidium) Extra" + }, + { + "mod_id": "sodiumoptionsapi", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mafglib", + "type": "client_only", + "reason": "通过JAR配置自动识别: MaFgLib", + "mod_name": "MaFgLib", + "confirmed": true + }, + { + "mod_id": "unicodefix", + "type": "client_only", + "reason": "", + "mod_name": "Unicode Fix" + }, + { + "mod_id": "zume", + "type": "client_only", + "reason": "", + "mod_name": "Zume" + }, + { + "mod_id": "relauncher", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "relauncher", + "confirmed": true + }, + { + "mod_id": "valkyrie", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Valkyrien Skies", + "confirmed": true + }, + { + "mod_id": "renderlib", + "type": "client_only", + "reason": "", + "mod_name": "RenderLib" + }, + { + "mod_id": "smoothfont", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "screenshot_viewer", + "type": "client_only", + "reason": "", + "mod_name": "ScreenShotViewer" + }, + { + "mod_id": "resourceloader", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "ResourceLoader", + "confirmed": true + }, + { + "mod_id": "neonium", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "neverenoughanimations", + "type": "client_only", + "reason": "", + "mod_name": "NeverEnoughAnimation" + }, + { + "mod_id": "nonconflictkeys", + "type": "client_only", + "reason": "", + "mod_name": "NonConflictKeys" + }, + { + "mod_id": "particleculling", + "type": "client_only", + "reason": "", + "mod_name": "ParticleCulling 1.8.9 port" + }, + { + "mod_id": "modernsplash", + "type": "client_only", + "reason": "", + "mod_name": "Modern Splash" + }, + { + "mod_id": "inputmethodblocker", + "type": "client_only", + "reason": "", + "mod_name": "InputMethodBlocker (Legacy)" + }, + { + "mod_id": "itemzoom", + "type": "client_only", + "reason": "", + "mod_name": "Item Zoomer" + }, + { + "mod_id": "gogskybox", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "gnetum", + "type": "client_only", + "reason": "通过JAR配置自动识别: Gnetum", + "mod_name": "Gnetum", + "confirmed": true + }, + { + "mod_id": "chatheadsyg", + "type": "client_only", + "reason": "", + "mod_name": "ChatHeads" + }, + { + "mod_id": "farsight", + "type": "client_only", + "reason": "通过JAR配置自动识别: Farsighted Mobs", + "mod_name": "Farsighted Mobs", + "confirmed": true + }, + { + "mod_id": "holdmyitems", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "3dskinlayers", + "type": "client_only", + "reason": "", + "mod_name": "3D Skin Layers" + }, + { + "mod_id": "bedbugs", + "type": "client_only", + "reason": "", + "mod_name": "Ceebug's Rounded Hotbar" + }, + { + "mod_id": "blur", + "type": "client_only", + "reason": "", + "mod_name": "Blur+" + }, + { + "mod_id": "celeritas", + "type": "client_only", + "reason": "", + "mod_name": "Celeritas Dynamic Lights" + }, + { + "mod_id": "rebind_narrator", + "type": "client_only", + "reason": "", + "mod_name": "RebindNarrator" + }, + { + "mod_id": "inventoryprofilesnext", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "customskinloader_forgev2", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "customskinloader_forgev1", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "notreepunching", + "type": "client_only", + "reason": "", + "mod_name": "NoTreePunching Basalt Fix (ARCHIVED)" + }, + { + "mod_id": "toadlib", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "ToadLib", + "confirmed": true + }, + { + "mod_id": "tweakerge", + "type": "client_only", + "reason": "通过JAR配置自动识别: Tweakerge", + "mod_name": "Tweakerge", + "confirmed": true + }, + { + "mod_id": "yeetusexperimentus", + "type": "client_only", + "reason": "", + "mod_name": "Yeetus Experimentus" + }, + { + "mod_id": "skinlayers3d", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "3d-Skin-Layers", + "confirmed": true + }, + { + "mod_id": "entityculling", + "type": "client_only", + "reason": "", + "mod_name": "EntityCulling" + }, + { + "mod_id": "ok_zoomer", + "type": "client_only", + "reason": "", + "mod_name": "Ok Zoomer - It's Zoom!" + }, + { + "mod_id": "chat_heads", + "type": "client_only", + "reason": "", + "mod_name": "Chat Heads" + }, + { + "mod_id": "i18nupdatemod", + "type": "client_only", + "reason": "", + "mod_name": "I18nUpdateMod" + }, + { + "mod_id": "imblocker", + "type": "client_only", + "reason": "", + "mod_name": "IMBlocker" + }, + { + "mod_id": "jecharacters", + "type": "client_only", + "reason": "", + "mod_name": "Just Enough Characters" + }, + { + "mod_id": "flerovium", + "type": "client_only", + "reason": "通过JAR配置自动识别: Flerovium", + "mod_name": "Flerovium", + "confirmed": true + }, + { + "mod_id": "battlemusic", + "type": "client_only", + "reason": "", + "mod_name": "Battle Music" + }, + { + "mod_id": "biomemusic", + "type": "client_only", + "reason": "", + "mod_name": "Biome Music" + }, + { + "mod_id": "toastcontrol", + "type": "client_only", + "reason": "", + "mod_name": "Toast Control [FABRIC]" + }, + { + "mod_id": "caelum", + "type": "client_only", + "reason": "", + "mod_name": "Caelum - ArdaCraft Edition" + }, + { + "mod_id": "beb", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "bettertaskbar", + "type": "client_only", + "reason": "", + "mod_name": "Better Taskbar" + }, + { + "mod_id": "bouncierbeds", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Bouncier Beds", + "confirmed": true + }, + { + "mod_id": "extrasoundsnext", + "type": "client_only", + "reason": "", + "mod_name": "ExtraSounds Next" + }, + { + "mod_id": "fallingleaves", + "type": "client_only", + "reason": "", + "mod_name": "Falling Leaves" + }, + { + "mod_id": "enchantmentdescriptions", + "type": "client_only", + "reason": "", + "mod_name": "Enchantment Descriptions" + }, + { + "mod_id": "legendarytooltips", + "type": "client_only", + "reason": "", + "mod_name": "Legendary Tooltips" + }, + { + "mod_id": "lanserverproperties", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "itemborders", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "ItemBorder", + "confirmed": true + }, + { + "mod_id": "gpumemleakfix", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Gpu memory leak fix mod", + "confirmed": true + }, + { + "mod_id": "freecam", + "type": "client_only", + "reason": "", + "mod_name": "Freecam" + }, + { + "mod_id": "fancymenu", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "FancyMenu", + "confirmed": true + }, + { + "mod_id": "cameraoverhaul", + "type": "client_only", + "reason": "", + "mod_name": "CameraOverhaul (Vintage)" + }, + { + "mod_id": "entity_model_features", + "type": "client_only", + "reason": "", + "mod_name": "Entity Model Features" + }, + { + "mod_id": "entity_texture_features", + "type": "client_only", + "reason": "", + "mod_name": "Entity Texture Features" + }, + { + "mod_id": "immersiveui", + "type": "client_only", + "reason": "", + "mod_name": "Immersive First Person" + }, + { + "mod_id": "presencefootsteps", + "type": "client_only", + "reason": "", + "mod_name": "Presence Footsteps" + }, + { + "mod_id": "entity_sound_features", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ruok", + "type": "client_only", + "reason": "", + "mod_name": "RuOK" + }, + { + "mod_id": "palladium", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Palladium", + "confirmed": true + }, + { + "mod_id": "sodiumdynamiclights", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "searchonmcmod", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "travelerstitles", + "type": "client_only", + "reason": "通过JAR配置自动识别: Traveler's Titles", + "mod_name": "Traveler's Titles", + "confirmed": true + }, + { + "mod_id": "visual_keybinder", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "datapackloaderrorfix", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "visuality", + "type": "client_only", + "reason": "", + "mod_name": "Visuality" + }, + { + "mod_id": "sounds", + "type": "client_only", + "reason": "", + "mod_name": "Sounds" + }, + { + "mod_id": "shouldersurfing", + "type": "client_only", + "reason": "", + "mod_name": "Shoulder Surfing Reloaded" + }, + { + "mod_id": "overloadedarmorbar", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "constantmusic", + "type": "client_only", + "reason": "", + "mod_name": "Constant Music" + }, + { + "mod_id": "bh", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "BH Creative", + "confirmed": true + }, + { + "mod_id": "namepain", + "type": "client_only", + "reason": "", + "mod_name": "Name Pain" + }, + { + "mod_id": "enhanced_boss_bars", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "melody", + "type": "client_only", + "reason": "", + "mod_name": "Melody" + }, + { + "mod_id": "reforgedplaymod", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "satisfying_buttons", + "type": "client_only", + "reason": "", + "mod_name": "Satisfying Buttons" + }, + { + "mod_id": "sakura", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Sakura Mod", + "confirmed": true + }, + { + "mod_id": "ftbchunks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "FTB Chunks" + }, + { + "mod_id": "forgelin_continuous", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Forgelin-Continuous" + }, + { + "mod_id": "multimob", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "tumbleweed", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Tumbleweed" + }, + { + "mod_id": "walljump", + "type": "client_and_server_required", + "reason": "", + "mod_name": "WallJumpVS" + }, + { + "mod_id": "foodexpansion1", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "sbm_bonetorch", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "skeletonhorsespawn", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mysticalworld", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "endreborn", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "raids_backport", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mysticallib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "pogosticks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "zettaigrimoires", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "goldfish", + "type": "client_and_server_required", + "reason": "根据网上资料修正(轻微差异): client_optional_server_required → client_and_server_required", + "mod_name": "" + }, + { + "mod_id": "camels", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "More Camels", + "confirmed": true + }, + { + "mod_id": "trinkets_and_baubles", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "benssharks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Ben's Sharks" + }, + { + "mod_id": "athelas", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "wild_netherwart", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "locks", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Locks!", + "confirmed": true + }, + { + "mod_id": "lovely_robot", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Reboot LovelyRobot" + }, + { + "mod_id": "setbonus", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Armor Set Bonuses" + }, + { + "mod_id": "bountiful", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Bountiful" + }, + { + "mod_id": "backport", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Vanilla Backport" + }, + { + "mod_id": "scalinghealth", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Scaling Health" + }, + { + "mod_id": "naturallychargedcreepers", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "stg", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Simple Transparent GUI [STG]", + "confirmed": true + }, + { + "mod_id": "wings", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Ears (+ Snouts/Muzzles, Tails, Horns, Wings, and More)", + "confirmed": true + }, + { + "mod_id": "xptome", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "mutantbeasts", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "simplecorn1", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "grimoireofgaia3", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "disenchanter1", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Disenchanter" + }, + { + "mod_id": "into_the_dungeons", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "specialmobs", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Special Mobs" + }, + { + "mod_id": "the_depths_of_madness", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "coralreef", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "coralreef", + "confirmed": true + }, + { + "mod_id": "surge", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Surge" + }, + { + "mod_id": "lavawaderbauble", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "into_the_end", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "endercrop", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "dragontweaks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "merchants", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Wandering Merchants", + "confirmed": true + }, + { + "mod_id": "fasterdonkeys", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "bettergolem", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "torchslabmod", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "bgs", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Midnighttigger's Better Grass", + "confirmed": true + }, + { + "mod_id": "somanyenchantments", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "deathfinder", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "DeathFinder", + "confirmed": true + }, + { + "mod_id": "defiledlands", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Defiled Lands" + }, + { + "mod_id": "strayspawn", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Stray Spawn", + "confirmed": true + }, + { + "mod_id": "oceanicexpanse", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Oceanic Expanse" + }, + { + "mod_id": "deep_below", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Deep Below" + }, + { + "mod_id": "villagercontracts", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Villager Contracts" + }, + { + "mod_id": "deeper_depths", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "cherry_on", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Cherry_on_1.12.2" + }, + { + "mod_id": "movillages", + "type": "client_and_server_required", + "reason": "", + "mod_name": "qrafty's Jungle Villages" + }, + { + "mod_id": "extrabows", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "morefurnaces", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "More Furnaces (Polymer)", + "confirmed": true + }, + { + "mod_id": "cxlibrary", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Formations (Structure Library)", + "confirmed": true + }, + { + "mod_id": "meldexun_scrystalicvoid", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "carianstyle", + "type": "client_and_server_required", + "reason": "", + "mod_name": "CarianStyle" + }, + { + "mod_id": "mooshroomspawn", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Mooshroom Spawn", + "confirmed": true + }, + { + "mod_id": "mapmaker_s_gadgets", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "spartanarmaments_v1hf1", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "enhancedarmaments", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Enhanced Armaments Reload Beams", + "confirmed": true + }, + { + "mod_id": "nether_api", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "forgelin", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Forgelin-Continuous" + }, + { + "mod_id": "silentlib", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "", + "confirmed": true + }, + { + "mod_id": "ctoasmod", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "spartanlightning", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "spartanweaponry", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "STONEBORN - Spartan Weaponry", + "confirmed": true + }, + { + "mod_id": "spartandefiled", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "spartanshields", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Spartan Shields" + }, + { + "mod_id": "chesttransporter", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "harvestersnight", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Harvester's Night" + }, + { + "mod_id": "mobrebirth", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Mob Rebirth", + "confirmed": true + }, + { + "mod_id": "keebszs_battle_towers", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Eternal Battletowers", + "confirmed": true + }, + { + "mod_id": "dghn2", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "creativecore", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "CreativeCore", + "confirmed": true + }, + { + "mod_id": "dyairdrop", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "engineersdecor", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Engineer's Decor" + }, + { + "mod_id": "damagenumbers", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Damage Numbers", + "confirmed": true + }, + { + "mod_id": "immersive_weathering", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Immersive Weathering" + }, + { + "mod_id": "diet", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Diet" + }, + { + "mod_id": "doomsday_decoration", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "wizardrynextgeneration", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "electroblobswizardry", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Electroblob's Wizardry Redux" + }, + { + "mod_id": "wizardryutils", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "huskspawn", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Husk Spawn", + "confirmed": true + }, + { + "mod_id": "sublime", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Sublime", + "confirmed": true + }, + { + "mod_id": "eyeofdragons", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "qualitytools", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "STONEBORN - Quality Tools", + "confirmed": true + }, + { + "mod_id": "llibrary", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Formations (Structure Library)", + "confirmed": true + }, + { + "mod_id": "rlartifacts", + "type": "client_and_server_required", + "reason": "", + "mod_name": "RLArtifacts" + }, + { + "mod_id": "useful_backpacks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Useful Backpacks" + }, + { + "mod_id": "blueprint", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Blueprint" + }, + { + "mod_id": "u_team_core", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "rainbowreef", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Rainbow Reef" + }, + { + "mod_id": "frozen_fiend", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ice_and_fire", + "type": "client_and_server_required", + "reason": "", + "mod_name": "IceAndFire Community Edition" + }, + { + "mod_id": "keletupackgears", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "wither_config", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Wither Config", + "confirmed": true + }, + { + "mod_id": "potioncore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "PotionCoreReloaded" + }, + { + "mod_id": "atlas_lib", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Atlas Lib", + "confirmed": true + }, + { + "mod_id": "boatdeletebegone", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "BoatDeleteBegone", + "confirmed": true + }, + { + "mod_id": "witherskeletontweaks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "nanfix_final_absorbtion", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "crafttweaker2", + "type": "client_and_server_required", + "reason": "", + "mod_name": "CraftTweaker" + }, + { + "mod_id": "fish_s_undead_rising", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "wizardrynecromancersdelight", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ftbquests", + "type": "client_and_server_required", + "reason": "", + "mod_name": "FTB Quests" + }, + { + "mod_id": "ftblib", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "FTB Library", + "confirmed": true + }, + { + "mod_id": "itemfilters", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Item Filters", + "confirmed": true + }, + { + "mod_id": "ftbmoney", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "FTB Money", + "confirmed": true + }, + { + "mod_id": "herobrinemod", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Herobrine Mobs", + "confirmed": true + }, + { + "mod_id": "libraryex", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "LibraryEx", + "confirmed": true + }, + { + "mod_id": "mospells", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Iron's Spells 'n Spellbooks" + }, + { + "mod_id": "minetweakerrecipemaker", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "beastslayer", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "netherex", + "type": "client_and_server_required", + "reason": "", + "mod_name": "NetherEx" + }, + { + "mod_id": "bountiful_baubles", + "type": "client_and_server_required", + "reason": "", + "mod_name": "BountifulBaubles:Reforked" + }, + { + "mod_id": "flamelib", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "FlameLib", + "confirmed": true + }, + { + "mod_id": "cloudboots", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Cloud Boots", + "confirmed": true + }, + { + "mod_id": "artificial_thunder", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "coroutil", + "type": "client_and_server_required", + "reason": "", + "mod_name": "CoroUtil" + }, + { + "mod_id": "ftb_ultimine", + "type": "client_and_server_required", + "reason": "", + "mod_name": "FTB Ultimine Cobblemon Compat" + }, + { + "mod_id": "obscure_api", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Obscure API" + }, + { + "mod_id": "red_core_mc", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "fugue", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Fugue" + }, + { + "mod_id": "add_potion", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Add Potion into Your Food", + "confirmed": true + }, + { + "mod_id": "caelus", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Caelus API" + }, + { + "mod_id": "cagedmobs", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Caged Mobs" + }, + { + "mod_id": "cialloblade", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "citadel_fix", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "combatnouveau", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Combat Nouveau" + }, + { + "mod_id": "culinaryconstruct", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Culinary Construct" + }, + { + "mod_id": "spears", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Simple Spears" + }, + { + "mod_id": "spoiled", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Spoiled" + }, + { + "mod_id": "dungeons_and_taverns_pillager_outpost_rework", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "dungeons_enhanced", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Dungeons Enhanced", + "confirmed": true + }, + { + "mod_id": "eureka", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Eureka! Ships! for Valkyrien Skies (Forge/Fabric)" + }, + { + "mod_id": "elainabroom", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "lionfishapi", + "type": "client_and_server_required", + "reason": "", + "mod_name": "lionfishapi" + }, + { + "mod_id": "lootjs", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "LootJS: KubeJS Addon", + "confirmed": true + }, + { + "mod_id": "legendarymonsters", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Legendary Monsters", + "confirmed": true + }, + { + "mod_id": "kubejs", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "KubeJS", + "confirmed": true + }, + { + "mod_id": "immersive_aircraft", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Immersive Aircraft" + }, + { + "mod_id": "carryon", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Carryon", + "confirmed": true + }, + { + "mod_id": "worldedit_mod", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "aquamirae", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Aquamirae" + }, + { + "mod_id": "valkyrienskies", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Valkyrien_Skies-defense" + }, + { + "mod_id": "playerrevive", + "type": "client_and_server_required", + "reason": "", + "mod_name": "PlayerRevive" + }, + { + "mod_id": "weather2", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Weather2 Additions" + }, + { + "mod_id": "irons_spellbooks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "iron's spellbooks arcane essence blocks" + }, + { + "mod_id": "toughasnails", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "ToughAsNails", + "confirmed": true + }, + { + "mod_id": "visualworkbench", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Visual Workbench" + }, + { + "mod_id": "upgrade_aquatic", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Upgrade Aquatic" + }, + { + "mod_id": "itemblacklist", + "type": "server_only", + "reason": "人工修改:InMain", + "mod_name": "Item Blacklist", + "confirmed": true + }, + { + "mod_id": "incineratorstryhard", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ironfurnaces", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Iron Furnaces" + }, + { + "mod_id": "invtweaks", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "InvTweaks", + "confirmed": true + }, + { + "mod_id": "ice_and_fire_delight", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ice_and_fire_spellbooks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "horsecombatcontrols", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "hitfeedback", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Hit Feedback" + }, + { + "mod_id": "framework", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Framework", + "confirmed": true + }, + { + "mod_id": "hotbath", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Hot Bath", + "confirmed": true + }, + { + "mod_id": "icarus", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Icarus" + }, + { + "mod_id": "iaf_patcher", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Ice And Fire Patcher", + "confirmed": true + }, + { + "mod_id": "goetyrevelation", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Goety: Revelation" + }, + { + "mod_id": "flib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "FLIB" + }, + { + "mod_id": "lootr", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Lootr" + }, + { + "mod_id": "simpledivinggear", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "simpleradio", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "sereneseasons", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Serene Seasons", + "confirmed": true + }, + { + "mod_id": "dreadsteel", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "gamediscs", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Game Discs" + }, + { + "mod_id": "minersglasses", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "pathfinderapi", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "PathFinder API", + "confirmed": true + }, + { + "mod_id": "exporbrecall", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ExpOrbRecall" + }, + { + "mod_id": "no_trampling_on_farmland", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ringsofascension", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ironchests", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Iron Chests: Restocked" + }, + { + "mod_id": "absolutelyunbreakable", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "commandsceptre", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "tarotcards", + "type": "client_and_server_required", + "reason": "", + "mod_name": "TarotCards: Remastered" + }, + { + "mod_id": "zenith", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Zenith" + }, + { + "mod_id": "fishermens_trap", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Fishermen's Trap [Neo/Fabric]" + }, + { + "mod_id": "glitchcore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "GlitchCore" + }, + { + "mod_id": "fishing_upgrades_more", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "farmingforblockheads", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Farming for Blockheads", + "confirmed": true + }, + { + "mod_id": "extrameat", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "hamsters", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Hamsters" + }, + { + "mod_id": "yakumoblade", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "wukong", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Epic Fight - Wukong Moveset", + "confirmed": true + }, + { + "mod_id": "zunpetforge", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "zetter", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Zetter — Painting Mod" + }, + { + "mod_id": "usefulspyglass", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Useful Spyglass" + }, + { + "mod_id": "yesstevemodel", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "wab", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Wan's Ancient Beasts", + "confirmed": true + }, + { + "mod_id": "tips", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Tips", + "confirmed": true + }, + { + "mod_id": "totw_modded", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "touhoulittlemaid", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Touhou Little Maid: Orihime" + }, + { + "mod_id": "toms_storage", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Tom's Simple Storage Mod" + }, + { + "mod_id": "tonsofenchants", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "takesapillage", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "tacz_fire_control_extension", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "tacz", + "type": "client_and_server_required", + "reason": "", + "mod_name": "[TaCZ] Timeless and Classics Zero" + }, + { + "mod_id": "taczlabs", + "type": "client_and_server_required", + "reason": "", + "mod_name": "[Tacz]Maxstuff" + }, + { + "mod_id": "taczaddon", + "type": "client_and_server_required", + "reason": "", + "mod_name": "taczaddonsfix" + }, + { + "mod_id": "subtleeffects", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Subtle Effects", + "confirmed": true + }, + { + "mod_id": "structure_gel", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "splash_milk", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Splash Milk", + "confirmed": true + }, + { + "mod_id": "spawnermod", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Enhanced Mob Spawners", + "confirmed": true + }, + { + "mod_id": "solcarrot", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Spice of Life: Carrot Edition" + }, + { + "mod_id": "soulslike_weaponry", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Marium's Soulslike Weaponry" + }, + { + "mod_id": "shutter", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Shutters" + }, + { + "mod_id": "simpletomb", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Simple Tomb", + "confirmed": true + }, + { + "mod_id": "skyarena", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Demi's Sky Arena", + "confirmed": true + }, + { + "mod_id": "shetiphiancore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ShetiPhianCore" + }, + { + "mod_id": "shadowizardlib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ShadowizardLib" + }, + { + "mod_id": "sherdsapi", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Sherds API" + }, + { + "mod_id": "rideeverything", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "riding_partners", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Riding Partners" + }, + { + "mod_id": "resourcefulconfig", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Resourceful Config" + }, + { + "mod_id": "relics", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Relics" + }, + { + "mod_id": "puzzleslib", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Puzzles Lib", + "confirmed": true + }, + { + "mod_id": "quick_refine", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "realmrpg_pots_and_mimics", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "propertymodifier", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "projectile_damage", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Projectile Damage Attribute", + "confirmed": true + }, + { + "mod_id": "prefab", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Prefab" + }, + { + "mod_id": "polymorph", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Polymorph" + }, + { + "mod_id": "portablehole", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "pickablepets", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Pickable Pets" + }, + { + "mod_id": "pillagers_gun", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Pillager’s Gun (Unofficial Port)" + }, + { + "mod_id": "patchouli", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Patchouli" + }, + { + "mod_id": "parcool", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ParCool!" + }, + { + "mod_id": "paintings", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Fast Paintings" + }, + { + "mod_id": "nocreeperexplosion", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "not_interested", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Not interested!" + }, + { + "mod_id": "octolib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ShatterLib | OctoLib" + }, + { + "mod_id": "naturescompass", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Nature's Compass" + }, + { + "mod_id": "moonlight", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Moonlight Lib" + }, + { + "mod_id": "refurbished_furniture", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "MrCrayfish's Furniture Mod: Refurbished", + "confirmed": true + }, + { + "mod_id": "mru", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "M.R.U", + "confirmed": true + }, + { + "mod_id": "multibeds", + "type": "client_and_server_required", + "reason": "", + "mod_name": "MultiBeds" + }, + { + "mod_id": "multimine", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Multimine", + "confirmed": true + }, + { + "mod_id": "mugging_villagers_mod", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mo_glass", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Mo Glass", + "confirmed": true + }, + { + "mod_id": "mine_treasure", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Mine Treasure" + }, + { + "mod_id": "mermod", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Mermod" + }, + { + "mod_id": "meetyourfight", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "l_enders_cataclysm", + "type": "client_and_server_required", + "reason": "", + "mod_name": "L_Ender's Cataclysm" + }, + { + "mod_id": "maidsoulkitchen", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Maidsoul Kitchen" + }, + { + "mod_id": "man_of_many_planes", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "enchanted_arsenal", + "type": "client_and_server_required", + "reason": "", + "mod_name": "[TACZ]Enchanted Arsenal" + }, + { + "mod_id": "explorerscompass_edited", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "fumo", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Fumo" + }, + { + "mod_id": "fzzy_config", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Fzzy Config" + }, + { + "mod_id": "glowingraidillagers", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "goety", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Goety - The Dark Arts" + }, + { + "mod_id": "goety_cataclysm", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Goety Cataclysm" + }, + { + "mod_id": "exposure", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Exposure" + }, + { + "mod_id": "exposure_catalog", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Exposure Catalog" + }, + { + "mod_id": "eclipticseasons", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Ecliptic Seasons" + }, + { + "mod_id": "eeeabsmobs", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "EEEAB's Mobs", + "confirmed": true + }, + { + "mod_id": "dummmmmmy", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "customstartinggear", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "dragonseeker", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Dragonseeker", + "confirmed": true + }, + { + "mod_id": "dragonfinder", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Ice and Fire: Dragon Finder" + }, + { + "mod_id": "disenchanting", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Easy Disenchanting" + }, + { + "mod_id": "cutthrough", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Cut Through", + "confirmed": true + }, + { + "mod_id": "comforts", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Comforts" + }, + { + "mod_id": "constructionwand", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Construction Wand", + "confirmed": true + }, + { + "mod_id": "cosmeticarmorreworked", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Cosmetic Armor Reworked", + "confirmed": true + }, + { + "mod_id": "clickadv", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Clickable Advancements", + "confirmed": true + }, + { + "mod_id": "cluttered", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Cluttered" + }, + { + "mod_id": "champions", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Champions" + }, + { + "mod_id": "call_of_drowner", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "celestial_artifacts", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "cerbonsapi", + "type": "client_and_server_required", + "reason": "", + "mod_name": "CERBON's API" + }, + { + "mod_id": "celestial_core", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "broomsmodunofficial", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "butcher", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Butcher's Delight" + }, + { + "mod_id": "bettertridents", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Better Tridents", + "confirmed": true + }, + { + "mod_id": "blackaures_paintings", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "bomd", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Bosses of Mass Destruction", + "confirmed": true + }, + { + "mod_id": "attributefix", + "type": "client_and_server_required", + "reason": "", + "mod_name": "AttributeFix" + }, + { + "mod_id": "badmobs", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Bad Mobs", + "confirmed": true + }, + { + "mod_id": "alcocraftplus", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "alexscaves", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Alexs Caves: Stuff & Torpedoes" + }, + { + "mod_id": "alexsdelight", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "alwayseat", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Always Eat" + }, + { + "mod_id": "artifacts", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Artifacts" + }, + { + "mod_id": "astikorcarts", + "type": "client_and_server_required", + "reason": "", + "mod_name": "AstikorCarts" + }, + { + "mod_id": "censoredasm5", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ctm", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "ConnectedTexturesMod", + "confirmed": true + }, + { + "mod_id": "wrapup", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "WrapUp", + "confirmed": true + }, + { + "mod_id": "fixeroo", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Fixeroo", + "confirmed": true + }, + { + "mod_id": "universaltweaks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Universal Tweaks" + }, + { + "mod_id": "supermartijn642corelib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "wanionlib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "WanionLib" + }, + { + "mod_id": "jaopca", + "type": "client_and_server_required", + "reason": "", + "mod_name": "JAOPCA" + }, + { + "mod_id": "scalar", + "type": "client_and_server_required", + "reason": "根据网上资料修正(轻微差异): client_required_server_optional → client_and_server_required", + "mod_name": "" + }, + { + "mod_id": "stellarcore", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "StellarCore", + "confirmed": true + }, + { + "mod_id": "topextras", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "TOP Extras", + "confirmed": true + }, + { + "mod_id": "tesla", + "type": "client_and_server_required", + "reason": "", + "mod_name": "TESLA" + }, + { + "mod_id": "jeivillagers", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "lunatriuscore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "item_filters", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Item Filters", + "confirmed": true + }, + { + "mod_id": "slashbladeresharped", + "type": "client_and_server_required", + "reason": "", + "mod_name": "SlashBlade:Resharped" + }, + { + "mod_id": "sjap_resharpened", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "ldip", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Dip Dye" + }, + { + "mod_id": "mysterious_mountain_lib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "fastworkbench", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "FastWorkbench", + "confirmed": true + }, + { + "mod_id": "taxfreelevels", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "connector", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Sinytra Connector", + "confirmed": true + }, + { + "mod_id": "justenoughadvancements", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "witherstormmod", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Netherite tools for Crackers witherstorm mod" + }, + { + "mod_id": "ftb_teams", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "FTB Teams", + "confirmed": true + }, + { + "mod_id": "ftb_quests", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "FTB Quests", + "confirmed": true + }, + { + "mod_id": "projecte", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ProjectE Integration" + }, + { + "mod_id": "teamprojecte", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "TeamProjectE", + "confirmed": true + }, + { + "mod_id": "sophisticatedcore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Sophisticated Core" + }, + { + "mod_id": "clumps", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Clumps", + "confirmed": true + }, + { + "mod_id": "watut", + "type": "client_and_server_required", + "reason": "", + "mod_name": "What Are They Up To (Watut)" + }, + { + "mod_id": "usefulslime", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Useful Slime" + }, + { + "mod_id": "ticex", + "type": "client_and_server_required", + "reason": "", + "mod_name": "TiCEX - Tinkers Construct EX" + }, + { + "mod_id": "projecte_integration", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ProjectE Integration" + }, + { + "mod_id": "prinegorerouse", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "libx", + "type": "client_and_server_required", + "reason": "", + "mod_name": "LibX" + }, + { + "mod_id": "packetfixer", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Packet Fixer" + }, + { + "mod_id": "slashblade_useful_addon", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "cupboard", + "type": "client_and_server_required", + "reason": "", + "mod_name": "cupboard" + }, + { + "mod_id": "balm", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Balm" + }, + { + "mod_id": "addonapi", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Moff's AddonAPI-DynLoad" + }, + { + "mod_id": "alltheleaks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "cwsm_v_sides", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "create", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Create" + }, + { + "mod_id": "appleskin", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "AppleSkin", + "confirmed": true + }, + { + "mod_id": "voicechat", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Simple Voice Chat", + "confirmed": true + }, + { + "mod_id": "carpet", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Carpet", + "confirmed": true + }, + { + "mod_id": "the_vault", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "The Vault", + "confirmed": true + }, + { + "mod_id": "tconstruct", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Tinkers' Construct", + "confirmed": true + }, + { + "mod_id": "optifine", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "OptiFine", + "confirmed": true + }, + { + "mod_id": "sodium", + "type": "client_only", + "reason": "", + "mod_name": "Sodium" + }, + { + "mod_id": "iris", + "type": "client_only", + "reason": "", + "mod_name": "Iris" + }, + { + "mod_id": "lithium", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Lithium", + "confirmed": true + }, + { + "mod_id": "phosphor", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Phosphor", + "confirmed": true + }, + { + "mod_id": "roughlyenoughitems", + "type": "client_required_server_optional", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "configuration", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Configuration", + "confirmed": true + }, + { + "mod_id": "waila", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "What am I looking at", + "confirmed": true + }, + { + "mod_id": "hwyla", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Hwyla", + "confirmed": true + }, + { + "mod_id": "jade", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Jade", + "confirmed": true + }, + { + "mod_id": "worldedit", + "type": "server_only", + "reason": "", + "mod_name": "WorldEdit" + }, + { + "mod_id": "worldguard", + "type": "server_only", + "reason": "", + "mod_name": "WorldGuard" + }, + { + "mod_id": "essentials", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Essentials", + "confirmed": true + }, + { + "mod_id": "luckperms", + "type": "server_only", + "reason": "", + "mod_name": "LuckPerms" + }, + { + "mod_id": "thermalexpansion", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Thermal Expansion" + }, + { + "mod_id": "thermalfoundation", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Thermal Foundation" + }, + { + "mod_id": "mekanism", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Mekanism" + }, + { + "mod_id": "enderio", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Ender IO", + "confirmed": true + }, + { + "mod_id": "ae2", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Applied Energistics 2" + }, + { + "mod_id": "refinedstorage", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Refined Storage" + }, + { + "mod_id": "ic2", + "type": "client_and_server_required", + "reason": "", + "mod_name": "IC2Classic" + }, + { + "mod_id": "buildcraft", + "type": "client_and_server_required", + "reason": "", + "mod_name": "BuildCraft" + }, + { + "mod_id": "forestry", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Forestry: Community Edition" + }, + { + "mod_id": "biomesoplenty", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Biomes O' Plenty", + "confirmed": true + }, + { + "mod_id": "twilightforest", + "type": "client_and_server_required", + "reason": "", + "mod_name": "The Twilight Forest" + }, + { + "mod_id": "aether", + "type": "client_and_server_required", + "reason": "", + "mod_name": "The Aether" + }, + { + "mod_id": "immersiveengineering", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Immersive Engineering" + }, + { + "mod_id": "botania", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Botania" + }, + { + "mod_id": "thaumcraft", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Thaumcraft", + "confirmed": true + }, + { + "mod_id": "bloodmagic", + "type": "client_and_server_required", + "reason": "", + "mod_name": "BloodMagic: Teams" + }, + { + "mod_id": "astralsorcery", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "chisel", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Chiseled Bookshelf Visualizer" + }, + { + "mod_id": "chiselsandbits", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "bibliocraft", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Bibliocraft Legacy" + }, + { + "mod_id": "decocraft", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Decocraft" + }, + { + "mod_id": "ironchest", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Iron Chests" + }, + { + "mod_id": "storagedrawers", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Storage Drawers" + }, + { + "mod_id": "extrautilities2", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "actuallyadditions", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Actually Additions" + }, + { + "mod_id": "harvestcraft", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Pam's HarvestCraft 2: Food Core" + }, + { + "mod_id": "cookingforblockheads", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mysticalagriculture", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Mystical Agriculture" + }, + { + "mod_id": "tinkerscomplement", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Tinkers' Complement" + }, + { + "mod_id": "mantle", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Mantle" + }, + { + "mod_id": "cofhcore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "FishyHard FHCore" + }, + { + "mod_id": "redstoneflux", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Redstone Flux", + "confirmed": true + }, + { + "mod_id": "baubles", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Trinkets and Baubles Reforked" + }, + { + "mod_id": "curios", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Curios API" + }, + { + "mod_id": "jeiintegration", + "type": "client_only", + "reason": "用户手动修改: client_required_server_optional → client_only", + "mod_name": "JEI Integration", + "confirmed": true + }, + { + "mod_id": "jeresources", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Just Enough Resources (JER)", + "confirmed": true + }, + { + "mod_id": "crafttweaker", + "type": "client_and_server_required", + "reason": "", + "mod_name": "CraftTweaker" + }, + { + "mod_id": "modtweaker", + "type": "client_and_server_required", + "reason": "", + "mod_name": "ModTweaker" + }, + { + "mod_id": "forgemultipart", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mcjtylib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "McJtyLib" + }, + { + "mod_id": "rftools", + "type": "client_and_server_required", + "reason": "", + "mod_name": "RFTools Base" + }, + { + "mod_id": "rftoolsdim", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "theoneprobe", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "topaddons", + "type": "client_and_server_required", + "reason": "用户手动修改: server_only → client_and_server_required", + "mod_name": "TOP Addons", + "confirmed": true + }, + { + "mod_id": "inventorytweaks", + "type": "client_only", + "reason": "", + "mod_name": "InventoryTweaks StationAPI" + }, + { + "mod_id": "mousetweaks", + "type": "client_only", + "reason": "", + "mod_name": "Mouse Tweaks" + }, + { + "mod_id": "controlling", + "type": "client_only", + "reason": "", + "mod_name": "Controlling" + }, + { + "mod_id": "defaultoptions", + "type": "client_only", + "reason": "", + "mod_name": "Default Options" + }, + { + "mod_id": "betterfoliage", + "type": "client_only", + "reason": "", + "mod_name": "Better Foliage Renewed" + }, + { + "mod_id": "dynamiclights", + "type": "client_required_server_optional", + "reason": "人工修改:InMain", + "mod_name": "DynamicLights", + "confirmed": true + }, + { + "mod_id": "soundfilters", + "type": "client_only", + "reason": "", + "mod_name": "Dynamic Sound Filters" + }, + { + "mod_id": "ambientsounds", + "type": "client_only", + "reason": "", + "mod_name": "AmbientSounds" + }, + { + "mod_id": "minimap", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Rei's Minimap", + "confirmed": true + }, + { + "mod_id": "antiqueatlas", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Antique Atlas", + "confirmed": true + }, + { + "mod_id": "damageindicators", + "type": "client_only", + "reason": "人工修改:InMain", + "mod_name": "Damage Indicators", + "confirmed": true + }, + { + "mod_id": "neat", + "type": "client_only", + "reason": "", + "mod_name": "Neat" + }, + { + "mod_id": "betterfps", + "type": "client_only", + "reason": "", + "mod_name": "Better FPS (Modpack)" + }, + { + "mod_id": "fastcraft", + "type": "client_only", + "reason": "", + "mod_name": "FastCraft" + }, + { + "mod_id": "foamfix", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "FoamFix", + "confirmed": true + }, + { + "mod_id": "vanillafix", + "type": "client_only", + "reason": "", + "mod_name": "VanillaFix" + }, + { + "mod_id": "textformatting", + "type": "client_optional_server_optional", + "reason": "人工修改:InMain", + "mod_name": "Text Formatting Everywhere", + "confirmed": true + }, + { + "mod_id": "chattweaks", + "type": "client_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "simplevoicechat", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Simple Voice Chat", + "confirmed": true + }, + { + "mod_id": "discordintegration", + "type": "server_only", + "reason": "", + "mod_name": "Discord_Integration" + }, + { + "mod_id": "servertabinfo", + "type": "server_only", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "morpheus", + "type": "client_optional_server_required", + "reason": "人工修改:InMain", + "mod_name": "Morpheus", + "confirmed": true + }, + { + "mod_id": "sleepingoverhaul", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Sleeping Overhaul 2", + "confirmed": true + }, + { + "mod_id": "corpse", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Corpse" + }, + { + "mod_id": "corpse", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Corpse" + }, + { + "mod_id": "gravestone", + "type": "client_and_server_required", + "reason": "", + "mod_name": "GraveStone Mod" + }, + { + "mod_id": "backpacks", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Backpacks", + "confirmed": true + }, + { + "mod_id": "ironbackpacks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "sophisticatedbackpacks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Sophisticated Backpacks" + }, + { + "mod_id": "travelersbackpack", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Traveler's Backpack" + }, + { + "mod_id": "waystones", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Waystones" + }, + { + "mod_id": "jmws", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "JourneyMap Waypoint Syncing", + "confirmed": true + }, + { + "mod_id": "fastleafdecay", + "type": "client_and_server_required", + "reason": "", + "mod_name": "FastLeafDecay" + }, + { + "mod_id": "treecapitator", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "TreeCapitator", + "confirmed": true + }, + { + "mod_id": "veinminer", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Veinminer" + }, + { + "mod_id": "oreexcavation", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Ore Excavation" + }, + { + "mod_id": "autoreglib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "AutoRegLib" + }, + { + "mod_id": "quark", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Quark" + }, + { + "mod_id": "charm", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Charm of Undying" + }, + { + "mod_id": "supplementaries", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Supplementaries" + }, + { + "mod_id": "decorativeblocks", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Decorative Blocks" + }, + { + "mod_id": "mcwfurniture", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Macaw's Furniture", + "confirmed": true + }, + { + "mod_id": "mcwdoors", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "mcwwindows", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "farmersdelight", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Farmer's Delight" + }, + { + "mod_id": "createaddition", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Create Crafts & Additions" + }, + { + "mod_id": "createcraftsadditions", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "computercraft", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Computer Craft", + "confirmed": true + }, + { + "mod_id": "opencomputers", + "type": "client_and_server_required", + "reason": "", + "mod_name": "OpenComputers" + }, + { + "mod_id": "securitycraft", + "type": "client_and_server_required", + "reason": "", + "mod_name": "SecurityCraft" + }, + { + "mod_id": "malisiscore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "tardismod", + "type": "client_and_server_required", + "reason": "", + "mod_name": "TARDIF Mod" + }, + { + "mod_id": "dimdoors", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Dimensional Doors", + "confirmed": true + }, + { + "mod_id": "compactmachines", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Compact Machines", + "confirmed": true + }, + { + "mod_id": "littletiles", + "type": "client_and_server_required", + "reason": "", + "mod_name": "LittleTiles" + }, + { + "mod_id": "chiseledme", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Chiseled Me", + "confirmed": true + }, + { + "mod_id": "animania", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Animania", + "confirmed": true + }, + { + "mod_id": "mocreatures", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Mo' Creatures", + "confirmed": true + }, + { + "mod_id": "", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Alex's Mobs", + "confirmed": true + }, + { + "mod_id": "iceandfire", + "type": "client_and_server_required", + "reason": "", + "mod_name": "IceAndFire Community Edition" + }, + { + "mod_id": "lycanitesmobs", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Lycanites Mobs", + "confirmed": true + }, + { + "mod_id": "primitivemobs", + "type": "client_and_server_required", + "reason": "", + "mod_name": "PrimitiveMobsRevival" + }, + { + "mod_id": "mowziesmobs", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "mod_name": "Mowzie's Mobs", + "confirmed": true + }, + { + "mod_id": "aquaculture", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Aquaculture Delight" + }, + { + "mod_id": "betteranimalsplus", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "natura", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Naturalist" + }, + { + "mod_id": "integrateddynamics", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Integrated Dynamics" + }, + { + "mod_id": "integratedtunnels", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Integrated Tunnels" + }, + { + "mod_id": "integratedterminals", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Integrated Terminals" + }, + { + "mod_id": "cyclopscore", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Cyclops Core" + }, + { + "mod_id": "commoncapabilities", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Common Capabilities" + }, + { + "mod_id": "placebo", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Placebo" + }, + { + "mod_id": "bookshelf", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Bookshelf" + }, + { + "mod_id": "citadel", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Citadel" + }, + { + "mod_id": "geckolib", + "type": "client_and_server_required", + "reason": "", + "mod_name": "GeckoLib 4" + }, + { + "mod_id": "architectury", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Architectury" + }, + { + "mod_id": "fabric_api", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Forgified Fabric API" + }, + { + "mod_id": "forge", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Forge Config API Port" + }, + { + "mod_id": "kotlinforforge", + "type": "client_and_server_required", + "reason": "", + "mod_name": "" + }, + { + "mod_id": "forgelin", + "type": "client_and_server_required", + "reason": "", + "mod_name": "Forgelin-Continuous" + }, + { + "mod_id": "amendments", + "mod_name": "Amendments", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "badpackets", + "mod_name": "Bad Packets", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "bits_n_bobs", + "mod_name": "Create Bits 'n' Bobs", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "clientsort", + "mod_name": "ClientSort", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "cmpackagecouriers", + "mod_name": "Create More: Package Couriers", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "aeronautics_bundled", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_enchantment_industry", + "mod_name": "Create: Enchantment Industry", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "createshufflefilter", + "mod_name": "Create Shuffle Filter", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_sa", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_dragons_plus", + "mod_name": "Create: Dragons Plus", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "createliquidfuel", + "mod_name": "Create Liquid Fuel", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "createoreexcavation", + "mod_name": "Create Ore Excavation", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_bic_bit", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_connected", + "mod_name": "Create: Connected", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_easy_structures", + "mod_name": "Create: Easy Structures", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_ltab", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_structures_arise", + "mod_name": "Create: Structures Arise", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "creeperheal", + "mod_name": "CreeperHeal", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "exposure_polaroid", + "mod_name": "Exposure Polaroid", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "flatbedrock", + "mod_name": "Flat Bedrock", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "ftblibrary", + "mod_name": "FTB Library", + "type": "client_and_server_required", + "reason": "人工修改:InMain", + "confirmed": true + }, + { + "mod_id": "gpu_tape", + "mod_name": "GPUTape", + "type": "client_only", + "reason": "通过JAR配置自动识别: GPUTape", + "confirmed": true + }, + { + "mod_id": "guideme", + "mod_name": "GuideME", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "immediatelyfast", + "mod_name": "ImmediatelyFast", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "industrial_platform", + "mod_name": "Industrial Platform", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "integrated_villages", + "mod_name": "Integrated Villages", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "irisflw", + "mod_name": "Iris Flywheel Compat", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "ixeris_dummy", + "mod_name": "Ixeris", + "type": "client_only", + "reason": "通过JAR配置自动识别: Ixeris", + "confirmed": true + }, + { + "mod_id": "mr_katters_structures", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "meme_mobs", + "mod_name": "Meme Mobs", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "owo", + "mod_name": "oωo", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "pointblank", + "mod_name": "Point Blank", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "prickle", + "mod_name": "PrickleMC", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "rolling_gate", + "mod_name": "RollingGate", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "silicone_dolls", + "mod_name": "SiliconeDolls", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "someassemblyrequired", + "mod_name": "Some Assembly Required", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "threadtweak", + "mod_name": "ThreadTweak", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "wwoo", + "mod_name": "William Wythers' Overhauled Overworld", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "zeta", + "mod_name": "Zeta", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "ftbteams", + "mod_name": "FTB Teams", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "betterendisland", + "mod_name": "YUNG's Better End Island", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "frostfire_dragon", + "mod_name": "Frostfire Dragon", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "luncheonmeatsdelight", + "mod_name": "Luncheon Meat's Delight", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "dungeons_arise", + "mod_name": "When Dungeons Arise", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "skyvillages", + "mod_name": "Sky Villages", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "muhc", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "untitledduckmod", + "mod_name": "Untitled Duck Mod", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "crystcursed_dragon", + "mod_name": "Crystcursed Dragon", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_central_kitchen", + "mod_name": "Create: Central Kitchen", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_mechanical_spawner", + "mod_name": "Create: Mechanical spawner", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_mobile_packages", + "mod_name": "Create: Mobile Packages", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "createfastschematiccannon", + "mod_name": "Create: Fast Schematic Cannon", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "createfisheryindustry", + "mod_name": "Create: Fishery Industry", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_currency_shops", + "mod_name": "Create: Currency Shops", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "create_cyber_goggles", + "mod_name": "Create: Cyber Goggles", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "kaleidoscope_cookery", + "mod_name": "Kaleidoscope Cookery", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "botanytrees", + "mod_name": "BotanyTrees", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "botanypots", + "mod_name": "BotanyPots", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "cataclysm", + "mod_name": "L_Ender's Cataclysm 1.21.1", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "creeper_firework", + "mod_name": "Creeper Firework", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "grindenchantments", + "mod_name": "Grind Enchantments", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "netmusic", + "mod_name": "Net Music Mod", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "wing_kirin", + "mod_name": "Wing Kirin", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "touhou_little_maid", + "mod_name": "Touhou Little Maid", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "ftbultimine", + "mod_name": "FTB Ultimine", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "farmersdelight_extended", + "mod_name": "Farmer's Delight: Extended", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "sodium_extra", + "mod_name": "Sodium Extra", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "enchdesc", + "mod_name": "", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "automobility", + "mod_name": "Automobility", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "brewinandchewin", + "mod_name": "Brewin' And Chewin'", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "lambdynlights", + "mod_name": "", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "another_furniture", + "mod_name": "Another Furniture", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "storagedelight", + "mod_name": "Storage Delight", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "placeholder-api", + "mod_name": "Placeholder API", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "createadditionallogistics", + "mod_name": "Create: Additional Logistics", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "createsifter", + "mod_name": "Create Sifter", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "modmenu", + "mod_name": "Mod Menu", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "immersive_melodies", + "mod_name": "Immersive Melodies", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "immersive_paintings", + "mod_name": "Immersive Paintings", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "sliceanddice", + "mod_name": "Create Slice & Dice", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "bbs", + "mod_name": "BBS CML Edition", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "colorwheel", + "mod_name": "Colorwheel", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "colorwheel_patcher", + "mod_name": "Colorwheel Patcher", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "flashback", + "mod_name": "Flashback", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "hold-my-items", + "mod_name": "Hold My Items", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "replaymod", + "mod_name": "Replay Mod", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "simplebackups", + "mod_name": "Simple Backups", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "watermedia", + "mod_name": "WaterMedia", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "displaydelight", + "mod_name": "Display Delight", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "c2me", + "mod_name": "Concurrent Chunk Management Engine", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "customskinloader", + "mod_name": "CustomSkinLoader", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "smoothchunk", + "mod_name": "Smooth chunk save Mod", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "shulkerboxtooltip", + "mod_name": "Shulker Box Tooltip", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "bettermounthud", + "mod_name": "Better Mount HUD", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "borderlessmining", + "mod_name": "Borderless Mining Updated", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "cape-provider", + "mod_name": "Cape Provider", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "cicada", + "mod_name": "CICADA", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "cloth-config", + "mod_name": "Cloth Config v26.1", + "type": "client_optional_server_optional", + "reason": "" + }, + { + "mod_id": "config_manager", + "mod_name": "Config Manager", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "crash_assistant", + "mod_name": "Crash Assistant", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "debugify", + "mod_name": "Debugify", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "do_a_barrel_roll", + "mod_name": "Do a Barrel Roll", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "dynamic_fps", + "mod_name": "Dynamic FPS", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "essential-container", + "mod_name": "", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "fabric-api", + "mod_name": "Fabric API", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "fabric-language-kotlin", + "mod_name": "Fabric Language Kotlin", + "type": "client_optional_server_optional", + "reason": "" + }, + { + "mod_id": "forgeconfigapiport", + "mod_name": "Forge Config API Port", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "isxander-main-menu-credits", + "mod_name": "", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "languagereload", + "mod_name": "Language Reload", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "malilib", + "mod_name": "MaLiLib", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "moogs_structures", + "mod_name": "Moog's Structure Lib", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "mvs", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "morechathistory", + "mod_name": "More Chat History", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "moreculling", + "mod_name": "More Culling", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "optigui", + "mod_name": "OptiGUI", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "puzzle", + "mod_name": "Puzzle", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "shogi", + "mod_name": "Shogi", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "sodium-extra", + "mod_name": "Sodium Extra", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "sspb", + "mod_name": "Sodium Shadowy Path Blocks", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "t_and_t", + "mod_name": "Towns and Towers", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "terralith", + "mod_name": "Terralith", + "type": "server_only", + "reason": "" + }, + { + "mod_id": "trinkets", + "mod_name": "Trinkets Continued", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "tweakeroo", + "mod_name": "Tweakeroo", + "type": "client_only", + "reason": "" + }, + { + "mod_id": "yet_another_config_lib_v3", + "mod_name": "", + "type": "client_and_server_required", + "reason": "" + }, + { + "mod_id": "zoomify", + "mod_name": "Zoomify", + "type": "client_only", + "reason": "" + } + ], + "total": 834, + "last_updated": "2026-04-30 18:19:43.344712" +} \ No newline at end of file diff --git a/docs/BUILD_GUIDE.md b/docs/BUILD_GUIDE.md index 7df0ad5..402ecd1 100644 --- a/docs/BUILD_GUIDE.md +++ b/docs/BUILD_GUIDE.md @@ -1,314 +1,77 @@ -# 打包指南 +# 打包构建指南 -## 📦 将Python程序打包为独立可执行文件 +## 📦 快速打包 -本文档介绍如何将Minecraft Mod Classifier Python版本打包为独立的可执行程序,无需安装Python即可运行。 - -## 🔧 打包工具 - -我们使用 **PyInstaller** 来打包Python程序: -- ✅ 跨平台支持(Windows/Linux/macOS) -- ✅ 单文件输出 -- ✅ 包含所有依赖 -- ✅ 用户无需安装Python - -## 🚀 快速打包 - -### Windows用户 - -1. **确保已安装Python 3.7+** - -2. **运行打包脚本** - ```cmd - build.bat - ``` - -3. **等待打包完成** - - 自动安装PyInstaller - - 编译可执行文件 - - 准备发布包 - -4. **获取结果** - ``` - release/Minecraft-mod-classifier/ - ├── Minecraft-mod-classifier.exe ← 主程序 - ├── README.md - ├── QUICKSTART.md - ├── LICENSE - ├── Input/ ← 放入待分类Mod - └── Output/ ← 分类结果 - ├── ClientOnly/ - ├── ServerOnly/ - └── ... - ``` - -### Linux/macOS用户 - -1. **确保已安装Python 3.7+** - -2. **添加执行权限并运行** - ```bash - chmod +x build.sh - ./build.sh - ``` - -3. **获取结果** - ``` - release/Minecraft-mod-classifier/ - ├── Minecraft-mod-classifier ← 主程序 - ├── README.md - ├── QUICKSTART.md - ├── LICENSE - ├── Input/ - └── Output/ - ``` - -## 📋 手动打包步骤 - -如果你想自定义打包过程: - -### 1. 安装PyInstaller +### Windows ```bash -pip install pyinstaller +scripts\build.bat ``` -### 2. 执行打包命令 +### Linux/macOS -**Windows:** -```cmd -pyinstaller --clean ^ - --name "Minecraft-mod-classifier" ^ - --onefile ^ - --console ^ - --add-data "mods_data.json;." ^ - main.py -``` - -**Linux/macOS:** ```bash -pyinstaller --clean \ - --name "Minecraft-mod-classifier" \ - --onefile \ - --console \ - --add-data "mods_data.json:." \ - main.py +chmod +x scripts/build.sh +./scripts/build.sh ``` -### 3. 参数说明 - -| 参数 | 说明 | -|------|------| -| `--clean` | 清理临时文件 | -| `--name` | 可执行文件名称 | -| `--onefile` | 打包为单个文件 | -| `--console` | 显示控制台窗口 | -| `--add-data` | 包含额外文件(格式:源文件;目标目录) | -| `--icon` | 设置图标(可选) | -| `--windowed` | 隐藏控制台(GUI程序用) | - -### 4. 准备发布包 +打包完成后,可执行文件位于 `dist/Minecraft-mod-classifier/` 目录。 -```bash -# 创建发布目录 -mkdir -p release/Minecraft-mod-classifier - -# 复制可执行文件 -cp dist/Minecraft-mod-classifier release/Minecraft-mod-classifier/ - -# 复制文档 -cp README.md QUICKSTART.md LICENSE release/Minecraft-mod-classifier/ +--- -# 创建必要目录 -mkdir -p release/Minecraft-mod-classifier/Input -mkdir -p release/Minecraft-mod-classifier/Output/{ClientOnly,ServerOnly,ClientRequiredServerOptional,ClientOptionalServerRequired,ClientAndServerRequired,ClientOptionalServerOptional,Unknown} -``` +## 🔧 手动打包 -### 5. 压缩发布包 +### 1. 安装 PyInstaller -**Windows:** -```cmd -cd release -Compress-Archive -Path Minecraft-mod-classifier -DestinationPath minecraft-mod-classifier-windows-x86_64.zip -``` - -**Linux/macOS:** ```bash -cd release -tar -czf minecraft-mod-classifier-linux-x86_64.tar.gz Minecraft-mod-classifier +pip install pyinstaller ``` -## 🎯 高级选项 - -### 减小文件大小 +### 2. 执行打包 ```bash -# 启用UPX压缩(需要先安装UPX) -pyinstaller --onefile --upx-dir=/path/to/upx main.py - -# 或使用内置压缩 -pyinstaller --onefile --strip main.py +python -m PyInstaller --name "Minecraft-mod-classifier" \ + --onedir --console \ + --add-data "config/mods_data.json;config" \ + src/python/main.py ``` -### 添加程序图标 +### 3. 创建压缩包 **Windows:** -```cmd -pyinstaller --onefile --icon=app.ico main.py -``` - -**macOS:** -```bash -pyinstaller --onefile --icon=app.icns main.py -``` - -### 隐藏控制台窗口(不推荐) - -```bash -pyinstaller --onefile --windowed main.py -``` - -⚠️ **注意**:本程序是命令行工具,不建议隐藏控制台。 - -### 包含多个数据文件 - -```bash -pyinstaller --onefile \ - --add-data "mods_data.json;." \ - --add-data "README.md;." \ - --add-data "assets/*;assets/" \ - main.py -``` - -## 🔍 常见问题 - -### Q1: 打包后的文件很大(50MB+)? - -**A:** 这是正常的,因为包含了Python解释器和所有依赖。 - -优化方法: -```bash -# 使用UPX压缩 -pip install upx -pyinstaller --onefile --upx-dir=/path/to/upx main.py - -# 或使用strip去除调试信息 -pyinstaller --onefile --strip main.py -``` - -### Q2: 打包后运行报错"找不到mods_data.json"? - -**A:** 确保使用了 `--add-data` 参数: - -```bash -# Windows ---add-data "mods_data.json;." - -# Linux/macOS ---add-data "mods_data.json:." -``` - -### Q3: 杀毒软件报毒? - -**A:** PyInstaller打包的程序可能被误报。解决方法: -1. 向杀毒软件厂商提交白名单 -2. 使用代码签名证书 -3. 提供源代码供用户自行编译 - -### Q4: 如何减小首次启动时间? - -**A:** 使用 `--onedir` 模式代替 `--onefile`: - -```bash -pyinstaller --onedir main.py +```powershell +Compress-Archive -Path dist\Minecraft-mod-classifier\* ` + -DestinationPath release\minecraft-mod-classifier-v0.1.6-windows-x86_64.zip ``` -优点:启动更快 -缺点:输出为目录而非单文件 - -### Q5: 跨平台打包? - -**A:** PyInstaller不支持跨平台打包,需要在目标平台上分别打包: -- Windows程序 → 在Windows上打包 -- Linux程序 → 在Linux上打包 -- macOS程序 → 在macOS上打包 - -可以使用GitHub Actions自动化多平台打包。 - -## 📊 打包结果对比 - -| 项目 | Python源码 | PyInstaller打包 | -|------|-----------|----------------| -| 文件大小 | ~50KB | ~15-50MB | -| 需要Python | ✅ 是 | ❌ 否 | -| 需要依赖 | ✅ 是 | ❌ 否 | -| 启动速度 | 快 | 稍慢(解压) | -| 跨平台 | ✅ 是 | ❌ 需分别打包 | -| 易用性 | 中 | 高 | - -## 🚀 GitHub Actions自动打包 - -项目已配置自动化打包工作流: - -1. **推送代码到main分支** -2. **GitHub Actions自动触发** -3. **在Windows和Linux上打包** -4. **上传为Artifacts** -5. **Release时自动发布** - -查看工作流配置:`.github/workflows/python-build.yml` - -## 📝 发布检查清单 - -发布前请确认: - -- [ ] 在目标平台测试可执行文件 -- [ ] 验证所有功能正常工作 -- [ ] 检查mods_data.json是否包含 -- [ ] 确认Input/Output目录结构正确 -- [ ] 包含必要的文档文件 -- [ ] 压缩为zip/tar.gz格式 -- [ ] 更新版本号 -- [ ] 编写Release Notes - -## 💡 最佳实践 - -### 1. 使用虚拟环境 - +**Linux/macOS:** ```bash -python -m venv venv -source venv/bin/activate # Linux/macOS -venv\Scripts\activate # Windows -pip install pyinstaller +cd dist +tar -czf minecraft-mod-classifier-v0.1.6-linux-x86_64.tar.gz Minecraft-mod-classifier/ ``` -### 2. 定期清理缓存 +--- -```bash -# 删除PyInstaller缓存 -rm -rf build dist *.spec -``` +## ❓ 常见问题 -### 3. 版本管理 +### Q: 打包后运行找不到配置文件 -在文件名中包含版本号: -```bash -pyinstaller --name "Minecraft-mod-classifier-v2.0.0" main.py -``` +**A:** 确保 `config/mods_data.json` 已包含在打包数据中(使用 `--add-data` 参数) -### 4. 测试不同系统 +### Q: 如何更新版本号 -在以下环境测试: -- Windows 10/11 -- Ubuntu 20.04/22.04 -- macOS 12/13 +**A:** 修改以下文件中的版本号: +- `README.md` +- `src/python/i18n.py` +- `docs/QUICKSTART.md` +- `GITHUB_INTEGRATION.md` -## 📚 相关资源 +### Q: 打包文件太大 -- [PyInstaller官方文档](https://pyinstaller.org/) -- [PyInstaller GitHub](https://github.com/pyinstaller/pyinstaller) -- [UPX压缩工具](https://upx.github.io/) +**A:** +- 使用 `--onefile` 单文件模式(启动稍慢) +- 启用 UPX 压缩(`--upx-dir=/path/to/upx`) --- -**打包完成!现在你可以分发独立的可执行文件了!** 🎉 +**详细文档**:[README.md](../README.md) diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md deleted file mode 100644 index 890a580..0000000 --- a/docs/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,171 +0,0 @@ -# 项目结构说明 - -## 📁 目录结构 - -``` -Minecraft-mod-classifier/ -│ -├── 📄 README.md # 项目主文档 -├── 📄 LICENSE # 许可证文件 -├── 📄 requirements.txt # Python依赖列表 -├── 📄 CMakeLists.txt # C++构建配置(保留供参考) -├── 📄 .gitignore # Git忽略规则 -│ -├── 📂 src/ # 源代码目录 -│ ├── 📂 python/ # Python源代码 -│ │ ├── __init__.py -│ │ ├── main.py # 主程序入口 -│ │ ├── mod_classifier.py # 核心分类逻辑 -│ │ ├── jar_parser.py # JAR包解析器 -│ │ ├── config_manager.py # 配置管理 -│ │ ├── file_utils.py # 文件工具 -│ │ ├── logger.py # 日志系统 -│ │ └── test.py # 测试脚本 -│ │ -│ └── 📂 cpp/ # C++源代码(保留供参考) -│ └── main.cpp -│ -├── 📂 scripts/ # 脚本文件 -│ ├── run.bat # Windows启动脚本 -│ ├── run.sh # Linux/macOS启动脚本 -│ ├── build.bat # Windows打包脚本 -│ ├── build.sh # Linux/macOS打包脚本 -│ └── test_build.bat # 快速测试打包脚本 -│ -├── 📂 config/ # 配置文件 -│ ├── mods_data.json # Mod分类配置数据库 -│ └── build.spec # PyInstaller打包配置 -│ -├── 📂 docs/ # 文档目录 -│ ├── QUICKSTART.md # 快速入门指南 ⭐ -│ ├── USAGE.md # 详细使用指南 -│ ├── BUILD_GUIDE.md # 打包指南 -│ ├── COMPARISON.md # C++ vs Python对比 -│ ├── MIGRATION.md # 迁移指南 -│ ├── PROJECT_SUMMARY.md # 项目技术总结 -│ ├── COMPLETION_SUMMARY.md # 完成总结 -│ ├── CHECKLIST.md # 检查清单 -│ ├── PACKAGING_COMPARISON.md # 打包方式对比 -│ ├── PACKAGING_QUICK_REF.md # 打包快速参考 -│ ├── PACKAGING_SUMMARY.md # 打包方案总结 -│ └── README_PYTHON.md # Python版本介绍 -│ -├── 📂 assets/ # 资源文件 -│ └── mods_data.json # 原始配置数据(备份) -│ -├── 📂 Input/ # 输入目录(运行时创建) -│ └── *.jar # 待分类的Mod文件 -│ -├── 📂 Output/ # 输出目录(运行时创建) -│ ├── ClientOnly/ -│ ├── ServerOnly/ -│ ├── ClientRequiredServerOptional/ -│ ├── ClientOptionalServerRequired/ -│ ├── ClientAndServerRequired/ -│ ├── ClientOptionalServerOptional/ -│ └── Unknown/ -│ -└── 📂 .github/ # GitHub配置 - └── workflows/ - ├── build.yml # C++版本CI/CD - └── python-build.yml # Python版本CI/CD -``` - -## 📋 目录说明 - -### `src/` - 源代码 -存放所有源代码文件。 - -**`src/python/`** -- Python版本的主要代码 -- 模块化设计,职责清晰 -- 包含完整的测试脚本 - -**`src/cpp/`** -- C++版本的原始代码 -- 保留供参考和对比 - -### `scripts/` - 脚本文件 -存放所有可执行脚本。 - -- **启动脚本**: `run.bat`, `run.sh` -- **打包脚本**: `build.bat`, `build.sh` -- **测试脚本**: `test_build.bat` - -### `config/` - 配置文件 -存放所有配置和数据文件。 - -- `mods_data.json`: Mod分类规则数据库 -- `build.spec`: PyInstaller打包配置 - -### `docs/` - 文档 -存放所有文档文件,按用途分类。 - -**核心文档:** -- `QUICKSTART.md` - 新用户必读 -- `USAGE.md` - 详细使用说明 -- `BUILD_GUIDE.md` - 打包指南 - -**技术文档:** -- `COMPARISON.md` - 版本对比 -- `PROJECT_SUMMARY.md` - 技术总结 -- `MIGRATION.md` - 迁移指南 - -**打包相关:** -- `PACKAGING_*.md` - 打包相关文档 - -### `assets/` - 资源文件 -存放静态资源文件(备份)。 - -### `Input/` 和 `Output/` - 运行时目录 -程序运行时自动创建的目录。 -- `Input/`: 放入待分类的Mod文件 -- `Output/`: 获取分类结果 - -## 🚀 快速导航 - -### 新手用户 -1. 阅读 [`README.md`](../README.md) -2. 查看 [`docs/QUICKSTART.md`](docs/QUICKSTART.md) -3. 运行 `scripts/run.bat` (Windows) 或 `scripts/run.sh` (Linux/macOS) - -### 开发者 -1. 查看 [`src/python/`](src/python/) 源代码 -2. 阅读 [`docs/PROJECT_SUMMARY.md`](docs/PROJECT_SUMMARY.md) -3. 运行 `scripts/test_build.bat` 测试打包 - -### 想要打包? -1. 阅读 [`docs/BUILD_GUIDE.md`](docs/BUILD_GUIDE.md) -2. 运行 `scripts/build.bat` (Windows) 或 `scripts/build.sh` (Linux/macOS) -3. 查看 [`docs/PACKAGING_QUICK_REF.md`](docs/PACKAGING_QUICK_REF.md) 快速获取帮助 - -## 📝 文件分类原则 - -| 文件类型 | 存放位置 | 示例 | -|---------|---------|------| -| 源代码 | `src/python/` | `.py` 文件 | -| 脚本 | `scripts/` | `.bat`, `.sh` 文件 | -| 配置 | `config/` | `.json`, `.spec` 文件 | -| 文档 | `docs/` | `.md` 文件 | -| 资源 | `assets/` | 静态资源文件 | -| 根目录 | 项目根目录 | `README.md`, `LICENSE` 等核心文件 | - -## 💡 最佳实践 - -### 添加新文档 -- 用户指南 → `docs/` -- 技术规范 → `docs/` -- 更新 `README.md` 中的相关链接 - -### 添加新脚本 -- 启动脚本 → `scripts/` -- 构建脚本 → `scripts/` -- 更新相关文档 - -### 添加新配置 -- 数据配置 → `config/` -- 构建配置 → `config/` 或项目根目录 - ---- - -**清晰的项目结构,让开发更高效!** 🎯 diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 3c14e73..332e39e 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -4,218 +4,58 @@ ### 版本说明 -当前版本:**v2.1.0** +当前版本:**v0.1.6** -**新增功能**: -- ✅ 多版本Mod管理(同一Mod的不同版本分别存储) -- ✅ Mod端识别(区分Fabric/Forge/NeoForge) -- ✅ 未知类型自动归类 +**核心功能**: +- ✅ 三层优先级分类 - JAR配置 > 规则数据库 > Modrinth API +- ✅ 自动学习机制 - 新Mod自动识别并保存 +- ✅ 智能规则更新 - 基于在线数据批量更新分类 +- ✅ 增量补丁生成 - 自动生成规则更新补丁 -### 第一步:检查Python环境 - -打开终端(Windows: CMD/PowerShell,Linux/macOS: Terminal),输入: - -```bash -python --version -``` - -或 - -```bash -python3 --version -``` - -**如果显示版本号(如 Python 3.9.7)** → 继续下一步 -**如果提示"未找到命令"** → 需要先安装Python - -#### 安装Python - -**Windows:** -1. 访问 https://www.python.org/downloads/ -2. 下载最新版本的Python -3. 运行安装程序 -4. ⚠️ **重要**:勾选 "Add Python to PATH" -5. 点击 "Install Now" - -**Linux (Ubuntu/Debian):** -```bash -sudo apt update -sudo apt install python3 -``` - -**macOS:** -```bash -brew install python3 -``` - -### 第二步:首次运行 - 选择语言 - -运行程序后,会首先询问界面语言: - -``` -================================================== -选择语言 / Select Language -================================================== -1. 中文 (Chinese) -2. English -================================================== -请选择 / Please select (1/2): -``` - -- 输入 `1` 选择中文 -- 输入 `2` 选择English - -语言设置会保存,下次运行无需再次选择。 - -### 第三步:准备Mod文件 - -1. 在项目文件夹中找到 `Input` 目录 -2. 将你要分类的 `.jar` Mod文件复制到 `Input` 目录 - -例如: -``` -Input/ -├── jei-1.16.5-7.7.1.118.jar -├── journeymap-1.12.2-5.6.0.jar -``` - -### 第四步:运行分类器 +### 第一步:运行程序 **Windows用户:** -- 双击 `run.bat` 文件 - -或在命令行中: ```cmd -python main.py +双击 run.bat ``` **Linux/macOS用户:** ```bash -python3 main.py +chmod +x run.sh +./run.sh ``` -### 第五步:查看结果 - -程序会自动: -1. ✅ 扫描 `Input` 目录中的所有JAR文件 -2. ✅ 清理文件名(去除版本号等信息) -3. ✅ 查询配置库或解析JAR文件 -4. ✅ 将Mod分类到 `Output` 的子目录中 +### 第二步:放入Mod文件 -查看分类结果: +将 `.jar` Mod文件复制到 `Input` 目录: ``` -Output/ -├── ClientOnly/ # 仅客户端Mod -│ └── journeymap-*.jar -├── ServerOnly/ # 仅服务端Mod -├── ClientRequiredServerOptional/ # 客户端必装,服务端可选 -│ └── jei-*.jar -├── ClientOptionalServerRequired/ # 客户端可选,服务端必装 -├── ClientAndServerRequired/ # 两端都必装 -├── ClientOptionalServerOptional/ # 两端都可选 -└── Unknown/ # 未知类型(需手动确认) -``` - -### 第六步:使用分类好的Mod - -从对应的子目录中取出Mod文件,放入你的Minecraft游戏目录: - -**客户端Mod** → `.minecraft/mods/` -**服务端Mod** → 服务器 `mods/` 目录 - -## 💡 小贴士 - -### 首次使用 vs 后续使用 - -**首次使用:** -- 程序会解析每个JAR文件 -- 速度较慢(约0.05秒/文件) -- 自动学习并保存新Mod信息 - -**后续使用:** -- 直接使用已保存的配置 -- 速度极快(约0.0001秒/文件) -- 仅新Mod需要解析 - -### 查看日志 - -所有操作都会记录在 `mod_classifier.log` 文件中: - -```bash -# Windows -type mod_classifier.log - -# Linux/macOS -cat mod_classifier.log +Input/ +├── jei-1.16.5.jar +├── journeymap-1.12.2.jar ``` -### 处理Unknown类型的Mod - -如果某些Mod被分类到 `Unknown` 目录: - -1. 查看日志了解原因 -2. 手动确定Mod类型 -3. 编辑 `mods_data.json` 添加正确分类 -4. 重新运行程序 +### 第三步:查看结果 -示例: -```json -[ - { - "name": "problematic_mod.jar", - "type": "client_only" - } -] +程序会自动分类到 `Output` 目录: ``` - -### 批量处理大量Mod - -如果有超过100个Mod: - -1. 分批处理(每次50-100个) -2. 等待首次解析完成 -3. 后续批次会更快 - -## ❓ 常见问题 - -### Q: 程序说"未找到JAR文件" - -**A:** 确保: -- Mod文件放在 `Input` 目录 -- 文件扩展名是 `.jar`(不是 `.jar.disabled`) - -### Q: 某些Mod分类错误 - -**A:** -1. 检查 `mod_classifier.log` 查看详情 -2. 手动编辑 `mods_data.json` 修正 -3. 提交Issue报告此Mod - -### Q: 如何清空重新开始? - -**A:** -```bash -# 删除输出目录 -rm -rf Output/ - -# 清空配置(保留备份) -cp mods_data.json mods_data_backup.json -echo "[]" > mods_data.json - -# 清空输入目录 -rm Input/*.jar +Output/ +├── ClientOnly/ # 仅客户端(小地图、光影) +├── ServerOnly/ # 仅服务端 +├── ClientRequiredServerOptional/ # 客户端必装(JEI) +├── ClientAndServerRequired/ # 双端必需 +└── Unknown/ # 需手动确认 ``` -### Q: 可以自定义分类类型吗? +### 第四步:使用分类好的Mod -**A:** 当前版本支持7种预定义类型。如需新增类型,请提交Feature Request. +- **客户端Mod** → `.minecraft/mods/` +- **服务端Mod** → 服务器 `mods/` 目录 -## 🎯 下一步 +--- -- 📖 阅读 [USAGE.md](USAGE.md) 了解高级用法 -- 🔍 查看 [PROJECT_SUMMARY.md](PROJECT_SUMMARY.md) 了解技术细节 -- 🤝 参与社区贡献,提交新的Mod分类规则 +## 💡 提示 ---- +**首次运行**:会解析JAR文件(较慢) +**后续运行**:直接使用已保存的配置(极快) -**祝你使用愉快!** 🎮✨ +**查看详细文档**:[README.md](../README.md) diff --git a/docs/USAGE.md b/docs/USAGE.md deleted file mode 100644 index dbba2ee..0000000 --- a/docs/USAGE.md +++ /dev/null @@ -1,210 +0,0 @@ -# 使用示例 - -## 快速开始 - -### Windows用户 - -1. 双击 `run.bat` 文件 -2. 或者在命令行中运行: - ```cmd - python main.py - ``` - -### Linux/macOS用户 - -1. 给脚本添加执行权限: - ```bash - chmod +x run.sh - ``` -2. 运行脚本: - ```bash - ./run.sh - ``` -3. 或者直接运行: - ```bash - python3 main.py - ``` - -## 工作流程示例 - -### 第一次使用 - -``` -程序启动 -├─→ 创建 Input/ 目录 -├─→ 创建 Output/ 目录及7个子目录 -├─→ 创建空的 mods_data.json -└─→ 等待用户在 Input/ 中放入Mod文件 -``` - -### 分类流程 - -假设 Input/ 目录中有以下文件: -``` -Input/ -├── jei-1.16.5-7.7.1.118.jar -├── journeymap-1.12.2-5.6.0.jar -└── optifine_1.16.5_hd_u_g8.jar -``` - -运行程序后的输出: -``` -============================================================ -开始分类Mod... -============================================================ - -处理: jei-1.16.5-7.7.1.118.jar -清理后的名称: jei.jar -配置中未找到,尝试解析JAR文件... -✓ 自动检测到类型: client_required_server_optional -✓ 已分类到: ClientRequiredServerOptional - -处理: journeymap-1.12.2-5.6.0.jar -清理后的名称: journeymap.jar -配置中未找到,尝试解析JAR文件... -✓ 自动检测到类型: client_only -✓ 已分类到: ClientOnly - -处理: optifine_1.16.5_hd_u_g8.jar -清理后的名称: optifine.jar -✓ 在配置中找到: client_only -✓ 已分类到: ClientOnly - -============================================================ -分类统计: -============================================================ -总文件数: 3 -成功分类: 3 -自动检测: 2 -跳过文件: 0 -分类失败: 0 -============================================================ -``` - -Output/ 目录结构: -``` -Output/ -├── ClientOnly/ -│ ├── journeymap-1.12.2-5.6.0.jar -│ └── optifine_1.16.5_hd_u_g8.jar -├── ClientRequiredServerOptional/ -│ └── jei-1.16.5-7.7.1.118.jar -├── ServerOnly/ -├── ClientOptionalServerRequired/ -├── ClientAndServerRequired/ -├── ClientOptionalServerOptional/ -└── Unknown/ -``` - -mods_data.json 已自动更新: -```json -[ - { - "name": "jei.jar", - "type": "client_required_server_optional" - }, - { - "name": "journeymap.jar", - "type": "client_only" - } -] -``` - -### 第二次使用(利用已学习的配置) - -当再次运行程序时: -- `jei.jar` 和 `journeymap.jar` 会直接从配置中读取,无需解析JAR -- 新的Mod文件会被自动检测和添加到配置中 - -## 高级用法 - -### 手动编辑配置文件 - -你可以直接编辑 `mods_data.json` 来: -- 修正自动检测错误的类型 -- 添加特殊的Mod规则 -- 批量导入已有的分类数据 - -格式示例: -```json -[ - { - "name": "mod_name.jar", - "type": "client_only" - }, - { - "name": "another_mod.jar", - "type": "client_and_server_required" - } -] -``` - -可用的类型值: -- `client_only` - 仅客户端 -- `server_only` - 仅服务端 -- `client_required_server_optional` - 客户端必装,服务端可选 -- `client_optional_server_required` - 客户端可选,服务端必装 -- `client_and_server_required` - 两端都必装 -- `client_optional_server_optional` - 两端都可选 -- `unknown` - 未知类型 - -### 查看日志 - -所有操作都会记录在 `mod_classifier.log` 文件中: -```bash -# Windows -type mod_classifier.log - -# Linux/macOS -cat mod_classifier.log -``` - -## 故障排除 - -### 问题1:Python未找到 - -**Windows:** -``` -[错误] 未检测到Python,请先安装Python 3.7或更高版本 -``` - -解决方案: -1. 访问 https://www.python.org/downloads/ -2. 下载并安装Python -3. 安装时勾选 "Add Python to PATH" - -**Linux:** -```bash -# Ubuntu/Debian -sudo apt install python3 - -# CentOS/RHEL -sudo yum install python3 -``` - -### 问题2:编码问题 - -如果遇到中文显示乱码: - -**Windows:** -```cmd -chcp 65001 -python main.py -``` - -或直接使用 `run.bat`(已自动设置编码) - -### 问题3:JAR解析失败 - -如果某个Mod被标记为 `Unknown`: -1. 检查 `mod_classifier.log` 查看详细错误 -2. 确认JAR文件未损坏 -3. 手动在 `mods_data.json` 中添加该Mod的分类 -4. 提交Issue报告此Mod的信息 - -## 性能提示 - -- 首次运行时,每个新Mod都需要解析JAR文件,可能较慢 -- 后续运行会直接使用配置库,速度显著提升 -- 对于大量Mod(100+),建议分批处理 -- 定期备份 `mods_data.json` 以保留学习成果 diff --git a/scripts/apply_online_rules.py b/scripts/apply_online_rules.py new file mode 100644 index 0000000..6af5e3c --- /dev/null +++ b/scripts/apply_online_rules.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""根据网上资料修正分类差异""" + +import json +import re + +def normalize_mod_id(name): + """统一命名规范""" + name = name.replace('.jar', '') + name = name.lower() + name = re.sub(r'[^a-z0-9_]', '_', name) + name = re.sub(r'_+', '_', name) + name = name.strip('_') + return name + +def load_rules_json(filepath): + """加载 rules.json""" + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + + rules = {} + for item in data: + mod_id_raw = item.get('mod_id', '') or item.get('name', '') + mod_type = item.get('type', '') + + if mod_id_raw and mod_type: + if mod_id_raw.endswith('.jar'): + mod_id = normalize_mod_id(mod_id_raw) + else: + mod_id = mod_id_raw.lower() + + rules[mod_id] = mod_type + + return rules + +def load_mod_rules_json(filepath): + """加载 mod_rules.json""" + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + + rules = {} + for item in data.get('rules', []): + mod_id = item.get('mod_id', '') + mod_type = item.get('type', '') + confirmed = item.get('confirmed', False) + if mod_id and mod_type: + rules[mod_id.lower()] = { + 'type': mod_type, + 'confirmed': confirmed + } + + return rules + +def is_major_difference(type1, type2): + """判断是否是重大差异 + + 重大差异:完全不同的类型(如 client_only vs server_only) + 轻微差异:四个可选类型之间的差异 + """ + # 定义四个可选类型 + optional_types = { + 'client_and_server_required', + 'client_optional_server_optional', + 'client_optional_server_required', + 'client_required_server_optional' + } + + # 如果两个都是可选类型,则是轻微差异 + if type1 in optional_types and type2 in optional_types: + return False + + # 否则是重大差异 + return True + +def main(): + rules_path = r'H:\code\Minecraft-mod-classifier\rules.json' + mod_rules_path = r'H:\code\Minecraft-mod-classifier\config\mod_rules.json' + + # 加载数据 + rules1 = load_rules_json(rules_path) # 网上资料 + rules2 = load_mod_rules_json(mod_rules_path) # 你的配置 + + print("=" * 80) + print("开始修正分类差异") + print("=" * 80) + print() + + # 找出所有差异(排除已确认的) + modifications = [] + + for key in sorted(rules1.keys()): + if key not in rules2: + continue + + type1 = rules1[key] # 网上资料 + type2_info = rules2[key] + type2 = type2_info['type'] + confirmed = type2_info['confirmed'] + + # 跳过已确认的 + if confirmed: + continue + + # 跳过相同的 + if type1 == type2: + continue + + # 判断差异程度 + major_diff = is_major_difference(type1, type2) + + modifications.append({ + 'mod_id': key, + 'online_type': type1, + 'current_type': type2, + 'major_diff': major_diff + }) + + print(f"找到 {len(modifications)} 个需要修正的差异") + print() + + # 统计 + major_count = sum(1 for m in modifications if m['major_diff']) + minor_count = len(modifications) - major_count + + print(f"重大差异: {major_count} 个(将添加锁死)") + print(f"轻微差异: {minor_count} 个(不添加锁死)") + print() + + # 询问是否执行 + response = input("是否执行修改?(y/n): ").strip().lower() + + if response != 'y': + print("已取消") + return + + # 加载完整配置 + with open(mod_rules_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + + rules_list = config_data.get('rules', []) + + # 执行修改 + modified_count = 0 + for mod in modifications: + mod_id = mod['mod_id'] + online_type = mod['online_type'] + major_diff = mod['major_diff'] + + # 查找对应的规则 + for rule in rules_list: + if rule.get('mod_id', '').lower() == mod_id: + # 修改 type + old_type = rule.get('type', '') + rule['type'] = online_type + + # 如果是重大差异,添加锁死 + if major_diff: + rule['confirmed'] = True + rule['reason'] = f"根据网上资料修正(重大差异): {old_type} → {online_type}" + else: + # 轻微差异,不锁死,但更新 reason + if not rule.get('reason'): + rule['reason'] = f"根据网上资料修正(轻微差异): {old_type} → {online_type}" + + modified_count += 1 + break + + # 保存文件 + with open(mod_rules_path, 'w', encoding='utf-8') as f: + json.dump(config_data, f, ensure_ascii=False, indent=2) + + print(f"\n已修正 {modified_count} 个模组的分类") + print(f"其中 {major_count} 个已添加锁死") + print(f"其中 {minor_count} 个未添加锁死") + +if __name__ == '__main__': + main() diff --git a/scripts/update_rules_with_mod_name.py b/scripts/update_rules_with_mod_name.py new file mode 100644 index 0000000..2694405 --- /dev/null +++ b/scripts/update_rules_with_mod_name.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +更新mod_rules.json脚本 +为现有规则添加mod_name字段(暂时为空) +""" + +import json +from pathlib import Path + + +def add_mod_name_field(rules_path: str = "config/mod_rules.json"): + """为mod_rules.json中的所有规则添加mod_name字段""" + + rules_file = Path(rules_path) + + if not rules_file.exists(): + print(f"文件不存在: {rules_file}") + return False + + try: + # 读取文件 + with open(rules_file, 'r', encoding='utf-8') as f: + data = json.load(f) + + rules = data.get('rules', []) + updated_count = 0 + + # 为每个规则添加mod_name字段 + for rule in rules: + if 'mod_name' not in rule: + rule['mod_name'] = '' # 暂时为空 + updated_count += 1 + + # 保存文件 + with open(rules_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + print(f"✅ 成功为 {updated_count} 条规则添加mod_name字段") + print(f"总规则数: {len(rules)}") + + return True + + except Exception as e: + print(f"❌ 更新失败: {str(e)}") + return False + + +if __name__ == "__main__": + print("正在更新 mod_rules.json...") + add_mod_name_field() + print("\n完成!") diff --git a/src/python/apply_patch.py b/src/python/apply_patch.py new file mode 100644 index 0000000..1599f6d --- /dev/null +++ b/src/python/apply_patch.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +应用增量补丁到规则数据库 +自动合并新增和更新的规则 +""" + +import json +import sys +from pathlib import Path + + +def apply_patch(patch_file: str, rules_file: str = "config/mod_rules.json"): + """ + 应用补丁到规则数据库 + + Args: + patch_file: 补丁文件路径 + rules_file: 规则数据库文件路径 + + Returns: + 是否成功 + """ + patch_path = Path(patch_file) + if not patch_path.exists(): + print(f"错误: 补丁文件 {patch_file} 不存在") + return False + + rules_path = Path(rules_file) + if not rules_path.exists(): + print(f"错误: 规则文件 {rules_file} 不存在") + return False + + try: + # 加载补丁 + with open(patch_path, 'r', encoding='utf-8') as f: + patch_data = json.load(f) + + # 加载规则数据库 + with open(rules_path, 'r', encoding='utf-8') as f: + rules_data = json.load(f) + + # 构建规则索引 + rules_index = {} + for i, rule in enumerate(rules_data.get('rules', [])): + mod_id = rule.get('mod_id', '') + if mod_id: + rules_index[mod_id] = (i, rule) + + new_count = 0 + updated_count = 0 + + # 处理新增规则 + for new_rule in patch_data.get('new_rules', []): + mod_id = new_rule.get('mod_id', '') + if mod_id and mod_id not in rules_index: + rules_data['rules'].append(new_rule) + rules_index[mod_id] = (len(rules_data['rules']) - 1, new_rule) + new_count += 1 + print(f" + 新增: {mod_id}") + + # 处理更新规则 + for update in patch_data.get('updated_rules', []): + mod_id = update.get('mod_id', '') + changes = update.get('changes', {}) + + if mod_id in rules_index: + idx, existing_rule = rules_index[mod_id] + + # 应用变更 + for field, change in changes.items(): + old_value = change.get('old', '') + new_value = change.get('new', '') + existing_rule[field] = new_value + + updated_count += 1 + print(f" ~ 更新: {mod_id} ({', '.join(changes.keys())})") + + # 保存更新后的规则数据库 + if new_count > 0 or updated_count > 0: + with open(rules_path, 'w', encoding='utf-8') as f: + json.dump(rules_data, f, ensure_ascii=False, indent=2) + + print(f"\n✓ 成功应用补丁!") + print(f" 新增规则: {new_count} 条") + print(f" 更新规则: {updated_count} 条") + print(f" 总规则数: {len(rules_data['rules'])} 条") + print(f"\n已保存到: {rules_file}") + return True + else: + print("\n⚠ 补丁中没有需要应用的变更") + return True + + except Exception as e: + print(f"✗ 应用补丁失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + +def main(): + """主函数""" + if len(sys.argv) < 2: + print("用法: python apply_patch.py <补丁文件>") + print("示例: python apply_patch.py rule_update_patch_20260430.json") + sys.exit(1) + + patch_file = sys.argv[1] + success = apply_patch(patch_file) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/src/python/config_manager.py b/src/python/config_manager.py index 7616ac1..3c0ec45 100644 --- a/src/python/config_manager.py +++ b/src/python/config_manager.py @@ -4,16 +4,20 @@ 配置管理模块 负责读取、保存和更新mods_data.json配置文件 -配置结构升级: -- 支持多版本:通过version字段区分 -- 支持多Mod端:通过loader字段区分(forge/fabric/neoforge) -- 唯一标识:name + version + loader +配置结构: +- mod_id: Mod的唯一标识符(必需) +- mod_name: Mod名称(可选) +- type: Mod类型(必需) +- reason: 分类原因说明(可选,默认为空字符串) +- 不再区分版本和加载器(运行位置通常不会改变) """ import json +import shutil from pathlib import Path from typing import List, Dict, Optional from logger import setup_logger +from file_utils import ensure_directory, get_resource_path logger = setup_logger() @@ -28,9 +32,22 @@ def __init__(self, config_path: str = "config/mods_data.json"): Args: config_path: 配置文件路径 """ - self.config_path = Path(config_path) + # 使用 get_resource_path 获取正确的文件路径 + self.config_path = get_resource_path(config_path) self.mods_data: List[Dict[str, str]] = [] self.logger = logger + + # 如果是打包环境且配置文件不存在,尝试从 _internal 复制初始配置 + import sys + if getattr(sys, 'frozen', False) and not self.config_path.exists(): + internal_config = Path(sys.executable).parent / '_internal' / config_path + if internal_config.exists(): + try: + ensure_directory(self.config_path.parent) + shutil.copy2(internal_config, self.config_path) + self.logger.info(f"从 _internal 复制初始配置文件") + except Exception as e: + self.logger.warning(f"复制初始配置文件失败: {str(e)}") def load_config(self) -> bool: """ @@ -45,7 +62,18 @@ def load_config(self) -> bool: try: with open(self.config_path, 'r', encoding='utf-8') as f: - self.mods_data = json.load(f) + data = json.load(f) + + # 兼容旧版本配置,确保包含mod_name和reason字段 + self.mods_data = [] + for mod in data: + simplified = { + 'mod_id': mod.get('mod_id', ''), + 'mod_name': mod.get('mod_name', ''), # 新增字段,默认为空 + 'type': mod.get('type', 'unknown'), + 'reason': mod.get('reason', '') # 新增字段,默认为空 + } + self.mods_data.append(simplified) self.logger.info(f"成功加载 {len(self.mods_data)} 条Mod配置") return True @@ -66,7 +94,7 @@ def save_config(self) -> bool: """ try: # 确保目录存在 - self.config_path.parent.mkdir(parents=True, exist_ok=True) + ensure_directory(self.config_path.parent) with open(self.config_path, 'w', encoding='utf-8') as f: json.dump(self.mods_data, f, ensure_ascii=False, indent=2) @@ -92,121 +120,76 @@ def create_default_config(self) -> bool: self.logger.error(f"创建默认配置失败: {str(e)}") return False - def _generate_mod_key(self, name: str, version: str = "", loader: str = "") -> str: + def find_mod(self, mod_id: str) -> Optional[Dict[str, str]]: """ - 生成Mod的唯一标识键 + 查找Mod配置(仅基于mod_id) Args: - name: Mod名称 - version: 版本号 - loader: Mod加载器类型 - - Returns: - 唯一标识键 - """ - key_parts = [name.lower()] - if version: - key_parts.append(version.lower()) - if loader: - key_parts.append(loader.lower()) - return "|".join(key_parts) - - def find_mod(self, clean_name: str, version: str = "", loader: str = "") -> Optional[Dict[str, str]]: - """ - 查找Mod配置(支持版本和Mod端匹配) - - Args: - clean_name: 清理后的Mod文件名(小写) - version: 版本号(可选) - loader: Mod加载器类型(可选) + mod_id: Mod的唯一标识符(modId) Returns: Mod配置字典,如果未找到返回None """ - target_key = self._generate_mod_key(clean_name, version, loader) - - # 首先尝试精确匹配(name + version + loader) for mod in self.mods_data: - mod_key = self._generate_mod_key( - mod.get('name', ''), - mod.get('version', ''), - mod.get('loader', '') - ) - if mod_key == target_key: + if mod.get('mod_id', '').lower() == mod_id.lower(): return mod - # 如果没有版本和loader信息,尝试仅按名称匹配 - if not version and not loader: - for mod in self.mods_data: - if mod.get('name', '').lower() == clean_name.lower(): - # 检查是否有更精确的匹配(有版本或loader) - # 如果有,优先使用无版本/无loader的记录 - if not mod.get('version') and not mod.get('loader'): - return mod - return None - def add_mod(self, name: str, mod_type: str, version: str = "", loader: str = "") -> bool: + def add_mod(self, mod_id: str, mod_type: str, mod_name: str = '') -> bool: """ 添加新的Mod配置 Args: - name: Mod名称 + mod_id: Mod的唯一标识符(modId) mod_type: Mod类型 - version: 版本号(可选) - loader: Mod加载器类型(可选) + mod_name: Mod名称(可选) Returns: 是否成功添加 """ # 检查是否已存在相同配置 - existing = self.find_mod(name, version, loader) + existing = self.find_mod(mod_id) if existing: - self.logger.debug(f"Mod {name} (v{version}, {loader}) 已存在于配置中") + self.logger.debug(f"Mod {mod_id} 已存在于配置中") return False new_mod = { - 'name': name, - 'type': mod_type + 'mod_id': mod_id, + 'mod_name': mod_name, + 'type': mod_type, + 'reason': '' # 初始化为空字符串,后续可填充 } - # 只在有值时添加version和loader字段 - if version: - new_mod['version'] = version - if loader: - new_mod['loader'] = loader - self.mods_data.append(new_mod) - - version_info = f" v{version}" if version else "" - loader_info = f" [{loader.upper()}]" if loader else "" - self.logger.info(f"添加新Mod配置: {name}{version_info}{loader_info} -> {mod_type}") + self.logger.info(f"添加新Mod配置: {mod_id} ({mod_name}) -> {mod_type}") return True - def update_mod(self, name: str, mod_type: str, version: str = "", loader: str = "") -> bool: + def update_mod(self, mod_id: str, mod_type: str, mod_name: str = '') -> bool: """ 更新Mod配置 Args: - name: Mod名称 + mod_id: Mod的唯一标识符(modId) mod_type: 新的Mod类型 - version: 版本号(可选) - loader: Mod加载器类型(可选) + mod_name: 新的Mod名称(可选) Returns: 是否成功更新 """ - target = self.find_mod(name, version, loader) + target = self.find_mod(mod_id) if target: old_type = target['type'] target['type'] = mod_type - - version_info = f" v{version}" if version else "" - loader_info = f" [{loader.upper()}]" if loader else "" - self.logger.info(f"更新Mod配置: {name}{version_info}{loader_info} ({old_type} -> {mod_type})") + if mod_name: + target['mod_name'] = mod_name + # 确保 reason 字段存在 + if 'reason' not in target: + target['reason'] = '' + self.logger.info(f"更新Mod配置: {mod_id} ({old_type} -> {mod_type})") return True - self.logger.warning(f"Mod {name} 不存在,无法更新") + self.logger.warning(f"Mod {mod_id} 不存在,无法更新") return False def get_mod_count(self) -> int: diff --git a/src/python/data_migration.py b/src/python/data_migration.py new file mode 100644 index 0000000..77c37b6 --- /dev/null +++ b/src/python/data_migration.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +数据转正工具 +将mods_data.json中的配置数据转入mod_rules.json规则数据库 + +用途: +1. 在全部分类完成后,将临时配置转正为永久规则 +2. 自动添加mod_name字段(从JAR解析获取) +3. 生成详细的分类原因说明 +""" + +import json +from pathlib import Path +from typing import List, Dict +from logger import setup_logger +from jar_parser import JarParser +from i18n import i18n +from file_utils import get_jar_files, ensure_directory + +logger = setup_logger() + + +class DataMigrationTool: + """数据迁移工具""" + + def __init__(self, + mods_data_path: str = "config/mods_data.json", + mod_rules_path: str = "config/mod_rules.json", + input_dir: str = "Input"): + """ + 初始化工具 + + Args: + mods_data_path: mods_data.json路径 + mod_rules_path: mod_rules.json路径 + input_dir: Input目录路径(用于重新解析JAR获取mod_name) + """ + self.mods_data_path = Path(mods_data_path) + self.mod_rules_path = Path(mod_rules_path) + self.input_dir = Path(input_dir) + self.jar_parser = JarParser() + self.logger = logger + + def load_mods_data(self) -> List[Dict]: + """加载mods_data.json""" + if not self.mods_data_path.exists(): + self.logger.error(f"文件不存在: {self.mods_data_path}") + return [] + + try: + with open(self.mods_data_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self.logger.info(f"成功加载 {len(data)} 条mods_data记录") + return data + + except Exception as e: + self.logger.error(f"加载mods_data.json失败: {str(e)}") + return [] + + def load_mod_rules(self) -> Dict: + """加载mod_rules.json""" + if not self.mod_rules_path.exists(): + self.logger.warning(f"规则文件不存在,将创建新文件: {self.mod_rules_path}") + return { + "version": "1.0.0", + "description": "Mod分类规则数据库 - 从历史配置转换而来", + "rules": [] + } + + try: + with open(self.mod_rules_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self.logger.info(f"成功加载 {len(data.get('rules', []))} 条规则") + return data + + except Exception as e: + self.logger.error(f"加载mod_rules.json失败: {str(e)}") + return { + "version": "1.0.0", + "description": "Mod分类规则数据库", + "rules": [] + } + + def get_mod_name_from_jar(self, mod_id: str) -> str: + """ + 从Input目录中的JAR文件解析mod_name + + Args: + mod_id: Mod ID + + Returns: + Mod名称,如果未找到返回空字符串 + """ + # 在Input目录中搜索包含mod_id的JAR文件 + if not self.input_dir.exists(): + return '' + + jar_files = get_jar_files(self.input_dir) + for jar_file in jar_files: + try: + mod_info = self.jar_parser.parse_jar(jar_file) + if mod_info and mod_info.get('mod_id', '').lower() == mod_id.lower(): + return mod_info.get('mod_name', '') + except Exception: + continue + + return '' + + def generate_reason(self, mod_type: str, mod_name: str = '') -> str: + """ + 生成分类原因说明 + + Args: + mod_type: Mod类型 + mod_name: Mod名称 + + Returns: + 原因说明文本 + """ + type_descriptions = { + 'client_only': '仅客户端需要,通常为界面优化、HUD、小地图等', + 'server_only': '仅服务端需要,通常为管理工具、性能优化等', + 'client_required_server_optional': '客户端必装,服务端可选,通常为JEI等辅助工具', + 'client_optional_server_required': '客户端可选,服务端必装,通常为世界生成Mod', + 'client_and_server_required': '两端都必须安装,通常为内容Mod、生物、物品等', + 'client_optional_server_optional': '两端都可选,通常为配置库、API库等', + 'unknown': '无法自动识别,需手动确认' + } + + base_reason = type_descriptions.get(mod_type, '未知类型') + + if mod_name: + return f"{base_reason} (Mod: {mod_name})" + else: + return base_reason + + def migrate_data(self, auto_detect_names: bool = True) -> bool: + """ + 执行数据迁移 + + Args: + auto_detect_names: 是否自动从JAR解析mod_name + + Returns: + 是否成功 + """ + print("\n" + "="*60) + print("开始数据转正流程") + print("="*60) + + # 1. 加载数据 + mods_data = self.load_mods_data() + if not mods_data: + self.logger.error("没有可迁移的数据") + return False + + mod_rules = self.load_mod_rules() + existing_rules = {rule['mod_id'].lower(): rule for rule in mod_rules.get('rules', [])} + + # 2. 转换数据 + new_rules = [] + updated_count = 0 + skipped_count = 0 + + for mod_entry in mods_data: + mod_id = mod_entry.get('mod_id', '') + mod_type = mod_entry.get('type', 'unknown') + mod_name = mod_entry.get('mod_name', '') + + if not mod_id: + self.logger.warning(f"跳过无效条目: {mod_entry}") + skipped_count += 1 + continue + + # 检查是否已存在 + if mod_id.lower() in existing_rules: + self.logger.debug(f"规则已存在,跳过: {mod_id}") + skipped_count += 1 + continue + + # 如果没有mod_name且启用了自动检测,尝试从JAR解析 + if not mod_name and auto_detect_names: + self.logger.info(f"正在解析 {mod_id} 的mod_name...") + mod_name = self.get_mod_name_from_jar(mod_id) + if mod_name: + self.logger.info(f" 找到mod_name: {mod_name}") + + # 生成规则 + reason = self.generate_reason(mod_type, mod_name) + + rule = { + "mod_id": mod_id, + "mod_name": mod_name, + "type": mod_type, + "reason": reason + } + + new_rules.append(rule) + updated_count += 1 + + # 3. 合并到现有规则 + mod_rules['rules'].extend(new_rules) + + # 4. 保存规则文件 + try: + ensure_directory(self.mod_rules_path.parent) + + with open(self.mod_rules_path, 'w', encoding='utf-8') as f: + json.dump(mod_rules, f, ensure_ascii=False, indent=2) + + self.logger.info(f"成功保存 {len(mod_rules['rules'])} 条规则到 {self.mod_rules_path}") + + # 输出统计信息 + print("\n" + "="*60) + print("数据转正完成!") + print("="*60) + print(f"新增规则: {updated_count}") + print(f"跳过(已存在): {skipped_count}") + print(f"总规则数: {len(mod_rules['rules'])}") + print("="*60) + + return True + + except Exception as e: + self.logger.error(f"保存规则文件失败: {str(e)}") + return False + + +def main(): + """主函数""" + tool = DataMigrationTool() + + print("\n此工具将把 mods_data.json 中的数据转入 mod_rules.json") + print("这将使临时配置变为永久规则。") + print() + + choice = input("是否继续? (y/n): ").strip().lower() + if choice not in ['y', 'yes', '是']: + print("已取消") + return + + success = tool.migrate_data(auto_detect_names=True) + + if success: + print("\n✅ 数据转正成功!") + else: + print("\n❌ 数据转正失败,请查看日志") + + input("\n按Enter键退出...") + + +if __name__ == "__main__": + main() diff --git a/src/python/file_utils.py b/src/python/file_utils.py index 2cfa7d5..e975c3b 100644 --- a/src/python/file_utils.py +++ b/src/python/file_utils.py @@ -2,139 +2,70 @@ # -*- coding: utf-8 -*- """ 文件工具模块 -提供文件名清理、目录创建等通用文件操作功能 +提供文件和目录操作的实用函数 """ -import re +import sys from pathlib import Path -from typing import Optional -from logger import setup_logger +from typing import List, Optional -logger = setup_logger() - -def clean_mod_name(full_filename: str) -> str: +def ensure_directory(dir_path: Path) -> bool: """ - 从完整的Mod文件名中提取干净的名称 - - 处理步骤: - 1. 移除方括号内的内容 [中文译名] - 2. 移除非标准分隔符 - 3. 处理混合语言前缀 - 4. 移除Minecraft版本号前缀 - 5. 移除 "for [加载器]" 模式 - 6. 迭代移除末尾的版本号、加载器等后缀 - 7. 规范化空格并转换为小写 + 确保目录存在,如果不存在则创建 Args: - full_filename: 完整的文件名(如 "jei-1.16.5-7.7.1.118.jar") + dir_path: 目录路径 Returns: - 清理后的文件名(如 "jei.jar") + 是否成功(True表示目录已存在或创建成功) """ - # 提取主文件名和扩展名 - path = Path(full_filename) - name_to_clean = path.stem - primary_extension = path.suffix - - # 特殊处理 .jar.disabled 等情况 - if '.jar' in full_filename.lower(): - jar_pos = full_filename.lower().rfind('.jar') - name_to_clean = full_filename[:jar_pos] - primary_extension = '.jar' - - # 1. 移除方括号内的内容 - name_to_clean = re.sub(r'\[[^\]]*\]', '', name_to_clean) - - # 2. 移除非标准分隔符(如 ·) - name_to_clean = name_to_clean.replace('·', '') - - # 3. 处理混合语言前缀(提取英文部分) - last_non_ascii_pos = -1 - for i in range(len(name_to_clean) - 1, -1, -1): - if ord(name_to_clean[i]) > 127: - last_non_ascii_pos = i - break - - if last_non_ascii_pos != -1 and last_non_ascii_pos + 1 < len(name_to_clean): - suffix_part = name_to_clean[last_non_ascii_pos + 1:] - if re.search(r'[a-zA-Z]', suffix_part): - name_to_clean = suffix_part - - # 4. 移除文件名开头的Minecraft版本号 - name_to_clean = re.sub(r'^[0-9]+\.[0-9]+(?:\.[0-9]+)*[-_]', '', name_to_clean, flags=re.IGNORECASE) - - # 5. 移除 "for [加载器或版本号]" 模式 - name_to_clean = re.sub(r'\s+for\s+[0-9a-zA-Z._-]+', '', name_to_clean, flags=re.IGNORECASE) - - # 6. 在加载器和数字之间插入空格 - loader_pattern = r'(forge|fabric|quilt|neoforge|rift|liteloader|nilloader)([0-9])' - name_to_clean = re.sub(loader_pattern, r'\1 \2', name_to_clean, flags=re.IGNORECASE) - - # 7. 迭代移除文件名末尾的版本号、加载器等后缀 - suffix_regex = ( - r'[-_+\s.]+' - r'(?:' - r'[a-zA-Z]{0,4}[0-9]+(?:[\._\-][0-9a-zA-Z_+-]+)*' - r'|mc[0-9]+(?:\.[0-9]+)*' - r'|forge|fabric|quilt|neoforge|rift|liteloader|nilloader' - r'|snapshot|pre|rc|beta|alpha|hotfix' - r'|universal|all|mc' - r')' - r'\s*$' - ) - - prev_name = "" - while name_to_clean != prev_name: - prev_name = name_to_clean - name_to_clean = re.sub(suffix_regex, '', name_to_clean, flags=re.IGNORECASE) - - # 8. 移除多余的空格,并修剪首尾空格和分隔符 - name_to_clean = re.sub(r' +', ' ', name_to_clean) - name_to_clean = name_to_clean.strip(' -_') - - # 9. 转换为小写 - name_to_clean = name_to_clean.lower() - - return name_to_clean + primary_extension + try: + dir_path.mkdir(parents=True, exist_ok=True) + return True + except Exception as e: + print(f"创建目录失败 {dir_path}: {str(e)}") + return False -def ensure_directory(directory_path: Path) -> bool: +def get_resource_path(relative_path: str) -> Path: """ - 确保目录存在,如果不存在则创建 + 获取资源文件的绝对路径(兼容开发环境和PyInstaller打包环境) Args: - directory_path: 目录路径 + relative_path: 相对路径(相对于项目根目录) Returns: - 是否成功确保目录存在 + 资源的绝对路径 """ - try: - directory_path.mkdir(parents=True, exist_ok=True) - return True - except Exception as e: - logger.error(f"无法创建目录 {directory_path}: {str(e)}") - return False + if getattr(sys, 'frozen', False): + # PyInstaller 打包后的环境 + # 可执行文件在 dist/Minecraft-mod-classifier/ + # 用户数据应该保存在可执行文件同级目录,而不是 _internal + base_path = Path(sys.executable).parent + else: + # 开发环境 + # 从 src/python/ 向上两级到项目根目录 + base_path = Path(__file__).parent.parent.parent + + return base_path / relative_path -def get_jar_files(input_dir: Path) -> list: +def get_jar_files(directory: Path) -> List[Path]: """ - 获取输入目录中的所有JAR文件 + 获取目录中所有的JAR文件 Args: - input_dir: 输入目录路径 + directory: 目标目录 Returns: JAR文件路径列表 """ - if not input_dir.exists(): - logger.warning(f"输入目录 {input_dir} 不存在") + if not directory.exists() or not directory.is_dir(): return [] - jar_files = [] - for file_path in input_dir.iterdir(): - if file_path.is_file() and file_path.suffix.lower() == '.jar': - jar_files.append(file_path) + jar_files = list(directory.glob('*.jar')) + # 按文件名排序 + jar_files.sort(key=lambda x: x.name.lower()) - logger.info(f"在 {input_dir} 中找到 {len(jar_files)} 个JAR文件") return jar_files diff --git a/src/python/generate_patch.py b/src/python/generate_patch.py new file mode 100644 index 0000000..61e1c7c --- /dev/null +++ b/src/python/generate_patch.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +生成规则数据库增量补丁 +只包含新增和更新的规则,便于审查和合并 +""" + +import json +from pathlib import Path +from datetime import datetime +from file_utils import get_resource_path + + +def generate_incremental_patch( + source_config: str = "config/mods_data.json", + target_rules: str = "config/mod_rules.json", + output_patch: str = None +): + """ + 生成增量补丁文件 + + Args: + source_config: 源配置文件路径(mods_data.json) + target_rules: 目标规则文件路径(mod_rules.json) + output_patch: 输出补丁文件路径(可选) + + Returns: + 补丁文件路径 + """ + # 使用 get_resource_path 获取正确的文件路径 + config_path = get_resource_path(source_config) + if not config_path.exists(): + print(f"错误: 配置文件 {source_config} 不存在") + print(f" 尝试路径: {config_path}") + return None + + with open(config_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + + # 加载目标规则 + rules_path = get_resource_path(target_rules) + if not rules_path.exists(): + print(f"错误: 规则文件 {target_rules} 不存在") + print(f" 尝试路径: {rules_path}") + return None + + with open(rules_path, 'r', encoding='utf-8') as f: + rules_data = json.load(f) + + # 构建规则索引 + rules_index = {} + for rule in rules_data.get('rules', []): + mod_id = rule.get('mod_id', '') + if mod_id: + rules_index[mod_id] = rule + + # 找出需要新增或更新的规则 + new_rules = [] + updated_rules = [] + + for mod_config in config_data: + mod_id = mod_config.get('mod_id', '') + mod_name = mod_config.get('mod_name', '') + mod_type = mod_config.get('type', 'unknown') + + if not mod_id: + continue + + if mod_id not in rules_index: + # 新规则 + new_rules.append({ + 'mod_id': mod_id, + 'mod_name': mod_name, + 'type': mod_type, + 'reason': '' # reason字段留空,待后期填充 + }) + else: + # 获取现有规则 + existing = rules_index[mod_id] + + # 检查是否为已确认配置,如果是则跳过 + if existing.get('confirmed', False): + continue + + # 检查是否需要更新 + changes = {} + + # 检查mod_name + if mod_name and (not existing.get('mod_name') or existing['mod_name'] != mod_name): + changes['mod_name'] = { + 'old': existing.get('mod_name', ''), + 'new': mod_name + } + + # 检查type + if mod_type and existing.get('type') != mod_type: + changes['type'] = { + 'old': existing.get('type', ''), + 'new': mod_type + } + + # 检查reason(如果为空则填充) + if not existing.get('reason') and mod_name: + changes['reason'] = { + 'old': '', + 'new': f'通过JAR配置自动识别: {mod_name}' + } + + if changes: + updated_rules.append({ + 'mod_id': mod_id, + 'changes': changes + }) + + # 如果没有变更,提示用户 + if not new_rules and not updated_rules: + print("\n✓ 规则数据库已是最新,无需生成补丁") + return None + + # 生成补丁文件 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + if output_patch is None: + output_patch = f"rule_update_patch_{timestamp}.json" + + patch_data = { + "version": "2.0", + "generated_at": datetime.now().isoformat(), + "description": "Minecraft Mod Classifier 规则数据库增量更新补丁", + "summary": { + "total_changes": len(new_rules) + len(updated_rules), + "new_rules": len(new_rules), + "updated_rules": len(updated_rules) + }, + "new_rules": new_rules, + "updated_rules": updated_rules, + "merge_instructions": [ + "如何合并此补丁:", + "1. 将补丁文件放到项目根目录", + "2. 运行: python src/python/apply_patch.py <补丁文件名>", + "3. 检查合并结果", + "4. 提交更新后的 config/mod_rules.json", + "", + "或者手动合并:", + "- 新增规则:添加到 config/mod_rules.json 的 rules 数组末尾", + "- 更新规则:找到对应的 mod_id,应用 changes 中的变更" + ] + } + + # 保存补丁文件 + with open(output_patch, 'w', encoding='utf-8') as f: + json.dump(patch_data, f, ensure_ascii=False, indent=2) + + print(f"\n✓ 成功生成增量补丁: {output_patch}") + print(f" 新增规则: {len(new_rules)} 条") + print(f" 更新规则: {len(updated_rules)} 条") + print(f" 总计变更: {len(new_rules) + len(updated_rules)} 条") + print(f"\n📤 如何提交更新:") + print(f" 方法1(推荐 - 使用合并工具):") + print(f" python src/python/apply_patch.py {output_patch}") + print(f"\n 方法2(手动 - 通过GitHub Issue):") + print(f" 1. 打开 GitHub Issues") + print(f" 2. 创建新Issue,标题:规则数据库更新 - {datetime.now().strftime('%Y-%m-%d')}") + print(f" 3. 将此JSON文件内容粘贴到Issue中") + print(f" 4. 维护者会使用工具自动合并") + + return output_patch + + +if __name__ == "__main__": + generate_incremental_patch() diff --git a/src/python/github_integration.py b/src/python/github_integration.py new file mode 100644 index 0000000..9f5d3c8 --- /dev/null +++ b/src/python/github_integration.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +GitHub集成模块 +提供创建GitHub Issue的功能(可选) +注意:此功能需要GitHub账号和Token,不是必需的 +""" + +import json +from pathlib import Path +from typing import Optional, Dict, List +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from logger import setup_logger + +logger = setup_logger() + + +class GitHubIntegration: + """GitHub集成管理器(可选功能)""" + + GITHUB_API_URL = "https://api.github.com" + + def __init__(self): + self.logger = logger + self.repo_info = self._get_repo_info() + + def _get_repo_info(self) -> Optional[Dict[str, str]]: + """ + 从git remote获取仓库信息(如果可用) + + Returns: + 包含owner和repo的字典,如果无法获取返回None + """ + import subprocess + try: + result = subprocess.run( + ['git', 'remote', 'get-url', 'origin'], + capture_output=True, + text=True, + cwd=Path.cwd(), + timeout=5 + ) + if result.returncode != 0: + return None + + remote_url = result.stdout.strip() + + # 解析GitHub URL (支持https和ssh格式) + if 'github.com' in remote_url: + if remote_url.startswith('git@'): + # SSH格式 + parts = remote_url.split(':') + path = parts[1].replace('.git', '') + else: + # HTTPS格式 + path = remote_url.split('github.com/')[1].replace('.git', '') + + parts = path.split('/') + if len(parts) >= 2: + return { + 'owner': parts[0], + 'repo': parts[1] + } + + return None + + except Exception: + # 如果没有git或获取失败,静默返回None + return None + + def create_github_issue(self, title: str, body: str, labels: List[str] = None) -> bool: + """ + 创建GitHub Issue + + Args: + title: Issue标题 + body: Issue内容 + labels: 标签列表 + + Returns: + 是否成功创建 + """ + if not self.repo_info: + self.logger.warning("无法获取仓库信息,请确保已配置git remote origin") + return False + + # 需要GitHub Token + import os + token = os.environ.get('GITHUB_TOKEN') + if not token: + self.logger.warning("未设置GITHUB_TOKEN环境变量,无法创建Issue") + print("\n提示: 创建GitHub Issue需要设置GITHUB_TOKEN环境变量") + print("请访问 https://github.com/settings/tokens 生成Personal Access Token") + print("并设置环境变量: set GITHUB_TOKEN=your_token_here (Windows)") + print("或: export GITHUB_TOKEN=your_token_here (Linux/Mac)") + return False + + try: + owner = self.repo_info['owner'] + repo = self.repo_info['repo'] + url = f"{self.GITHUB_API_URL}/repos/{owner}/{repo}/issues" + + data = { + 'title': title, + 'body': body + } + + if labels: + data['labels'] = labels + + # 发送请求 + req = Request(url) + req.add_header('Authorization', f'token {token}') + req.add_header('Content-Type', 'application/json') + req.add_header('User-Agent', 'Minecraft-Mod-Classifier/2.0.0') + req.data = json.dumps(data).encode('utf-8') + + with urlopen(req, timeout=10) as response: + result = json.loads(response.read().decode('utf-8')) + issue_url = result.get('html_url', '') + issue_number = result.get('number', 0) + self.logger.info(f"成功创建GitHub Issue #{issue_number}: {issue_url}") + print(f"\n✅ 成功创建GitHub Issue #{issue_number}") + print(f" {issue_url}") + return True + + except HTTPError as e: + if e.code == 401: + self.logger.error("GitHub Token无效或已过期") + print("\n❌ GitHub Token无效或已过期,请重新生成") + elif e.code == 404: + self.logger.error("仓库不存在或无权限") + print("\n❌ 仓库不存在或无权限") + else: + self.logger.error(f"HTTP错误 {e.code}: {str(e)}") + return False + except URLError as e: + self.logger.error(f"网络错误: {str(e)}") + return False + except Exception as e: + self.logger.error(f"创建Issue失败: {str(e)}") + return False + + def generate_issue_content(self, new_mods: List[Dict]) -> tuple: + """ + 生成Issue内容 + + Args: + new_mods: 新分类的Mod列表 + + Returns: + (title, body) 元组 + """ + from datetime import datetime + + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + title = f"自动分类报告 - {timestamp}" + + body = f"## 自动分类报告\n\n" + body += f"**生成时间**: {timestamp}\n\n" + body += f"**新增分类数量**: {len(new_mods)}\n\n" + body += f"---\n\n" + body += f"### 新增Mod分类\n\n" + body += f"| Mod ID | Mod Name | 类型 |\n" + body += f"|--------|----------|------|\n" + + for mod in new_mods: + mod_id = mod.get('mod_id', 'N/A') + mod_name = mod.get('mod_name', 'N/A') + mod_type = mod.get('type', 'unknown') + body += f"| {mod_id} | {mod_name} | {mod_type} |\n" + + body += f"\n---\n\n" + body += f"*此Issue由Minecraft Mod Classifier自动生成*\n" + + return title, body + + def save_issue_to_file(self, title: str, body: str, filename: str = None) -> str: + """ + 将Issue内容保存为Markdown文件,方便用户手动提交 + + Args: + title: Issue标题 + body: Issue内容 + filename: 文件名(可选) + + Returns: + 保存的文件路径 + """ + if filename is None: + from datetime import datetime + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"github_issue_{timestamp}.md" + + filepath = Path(filename) + + try: + content = f"# {title}\n\n{body}" + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + + self.logger.info(f"Issue内容已保存到: {filepath}") + print(f"\n📄 Issue内容已保存到: {filepath}") + print(f" 您可以打开此文件,复制内容到GitHub创建Issue") + return str(filepath) + + except Exception as e: + self.logger.error(f"保存Issue文件失败: {str(e)}") + return "" + + +def main(): + """测试函数""" + github = GitHubIntegration() + print("GitHubIntegration模块加载成功") + print(f"仓库信息: {github.repo_info}") + + +if __name__ == "__main__": + main() diff --git a/src/python/i18n.py b/src/python/i18n.py index 62cde47..ea1d20e 100644 --- a/src/python/i18n.py +++ b/src/python/i18n.py @@ -8,6 +8,7 @@ import json import os from pathlib import Path +from file_utils import ensure_directory class I18nManager: @@ -19,7 +20,7 @@ def __init__(self): 'zh': { # 程序信息 'app_name': 'Minecraft Mod 分类器', - 'app_version': 'v2.0.0', + 'app_version': 'v0.1.6', 'app_description': '自动分类 Minecraft Mod 文件的命令行工具', # 启动信息 @@ -78,11 +79,29 @@ def __init__(self): 'english': 'English', 'current_language': '当前语言', 'language_changed': '语言已切换', + 'yes': '是', + 'no': '否', + 'invalid_choice': '无效选择', + + # GitHub集成 + 'github_prompt_title': '📤 提交到GitHub', + 'github_prompt_description': '是否将更新后的mods_data.json提交到GitHub仓库?\n这将帮助社区共享Mod分类数据。', + 'github_not_git_repo': '当前目录不是Git仓库,无法提交', + 'github_no_changes': 'mods_data.json没有未提交的更改', + 'github_ask_submit': '是否提交到GitHub?', + 'github_ask_message': '请输入提交信息', + 'github_adding_file': '正在添加文件: {file}', + 'github_committing': '正在提交...', + 'github_pushing': '正在推送到远程仓库...', + 'github_success': '✅ 成功提交到GitHub!', + 'github_failed': '❌ 提交失败', + 'github_push_failed_manual': '推送失败,请手动执行 git push', + 'github_skipped': '已跳过提交', }, 'en': { # App info 'app_name': 'Minecraft Mod Classifier', - 'app_version': 'v2.0.0', + 'app_version': 'v0.1.6', 'app_description': 'Command-line tool for automatically classifying Minecraft Mod files', # Startup @@ -141,6 +160,24 @@ def __init__(self): 'english': 'English', 'current_language': 'Current language', 'language_changed': 'Language changed', + 'yes': 'Yes', + 'no': 'No', + 'invalid_choice': 'Invalid choice', + + # GitHub Integration + 'github_prompt_title': '📤 Submit to GitHub', + 'github_prompt_description': 'Submit the updated mods_data.json to GitHub repository?\nThis helps the community share mod classification data.', + 'github_not_git_repo': 'Current directory is not a Git repository, cannot commit', + 'github_no_changes': 'No uncommitted changes in mods_data.json', + 'github_ask_submit': 'Submit to GitHub?', + 'github_ask_message': 'Enter commit message', + 'github_adding_file': 'Adding file: {file}', + 'github_committing': 'Committing...', + 'github_pushing': 'Pushing to remote repository...', + 'github_success': '✅ Successfully committed to GitHub!', + 'github_failed': '❌ Commit failed', + 'github_push_failed_manual': 'Push failed, please run git push manually', + 'github_skipped': 'Skipped submission', } } @@ -161,7 +198,7 @@ def _load_settings(self): def save_settings(self): """保存用户设置""" settings_file = Path('config/settings.json') - settings_file.parent.mkdir(parents=True, exist_ok=True) + ensure_directory(settings_file.parent) settings = { 'language': self.current_language diff --git a/src/python/jar_parser.py b/src/python/jar_parser.py index 5ffe0ee..a0af937 100644 --- a/src/python/jar_parser.py +++ b/src/python/jar_parser.py @@ -3,6 +3,19 @@ """ JAR包配置文件解析器 从JAR文件中提取Mod元数据并判断类型 + +三层优先级判断(带验证机制): +A. JAR配置文件标识 (side/environment字段) - 最高优先级 +B. 规则数据库 (mod_rules.json) - 中等优先级 + - 如果规则的reason来自JAR配置 → 直接使用 + - 如果规则的reason来自API/手动 → 需要重新解析JAR验证(降级到优先级A) +C. Modrinth API检索 - 最低优先级 +无法判断则归类为unknown,由用户手动确认 + +补充设定: +有些模组API返回的信息可能与实际不一致,因此添加保底措施: +规则配置中"reason"非读取JAR配置文件得到的需要重新读取JAR进行验证。 +验证完毕后分类,最后同步信息修改至规则配置。 """ import zipfile @@ -11,6 +24,8 @@ from pathlib import Path from typing import Optional, Dict, Any from logger import setup_logger +from rule_manager import RuleManager +from modrinth_api import ModrinthAPI logger = setup_logger() @@ -28,8 +43,13 @@ class JarParser: ] MC_MOD_INFO = "mcmod.info" - def __init__(self): + def __init__(self, skip_rules: bool = False): self.logger = logger + self.rule_manager = RuleManager() + self.skip_rules = skip_rules # 是否跳过规则数据库检查(用于强制重新分类) + if not skip_rules: + self.rule_manager.load_rules() # 加载规则数据库 + self.modrinth_api = ModrinthAPI() # 初始化Modrinth API客户端 def parse_jar(self, jar_path: Path) -> Optional[Dict[str, Any]]: """ @@ -70,7 +90,7 @@ def parse_jar(self, jar_path: Path) -> Optional[Dict[str, Any]]: def _parse_fabric_mod(self, zip_file: zipfile.ZipFile) -> Optional[Dict[str, Any]]: """ - 解析Fabric Mod配置 + 解析Fabric fabric.mod.json配置 Args: zip_file: ZIP文件对象 @@ -80,16 +100,24 @@ def _parse_fabric_mod(self, zip_file: zipfile.ZipFile) -> Optional[Dict[str, Any """ try: with zip_file.open(self.FABRIC_MOD_JSON) as f: - data = json.load(f) + data = json.loads(f.read().decode('utf-8')) + + mod_id = data.get('id', '') + mod_name = data.get('name', '') + version = data.get('version', '') + + # 优先级A: 读取environment字段 (内部会先检查优先级B) + mod_type = self._infer_mod_type_from_fabric(data) mod_info = { - 'name': data.get('id', ''), - 'version': data.get('version', ''), - 'loader': 'fabric', # Fabric Mod - 'type': self._infer_mod_type_from_fabric(data) + 'mod_id': mod_id, + 'mod_name': mod_name, + 'version': version, + 'loader': 'fabric', + 'type': mod_type } - self.logger.debug(f"解析Fabric Mod: {mod_info['name']}") + self.logger.debug(f"解析Fabric Mod: {mod_id} ({mod_name})") return mod_info except Exception as e: @@ -98,11 +126,11 @@ def _parse_fabric_mod(self, zip_file: zipfile.ZipFile) -> Optional[Dict[str, Any def _parse_forge_mods_toml(self, zip_file: zipfile.ZipFile, toml_path: str = None) -> Optional[Dict[str, Any]]: """ - 解析Forge/NeoForge mods.toml配置(简化版,仅提取基本信息) + 解析Forge/NeoForge mods.toml配置 Args: zip_file: ZIP文件对象 - toml_path: TOML文件路径(默认为标准Forge路径) + toml_path: TOML文件路径 Returns: Mod信息字典 @@ -114,27 +142,82 @@ def _parse_forge_mods_toml(self, zip_file: zipfile.ZipFile, toml_path: str = Non with zip_file.open(toml_path) as f: content = f.read().decode('utf-8') - # 简单的TOML解析(实际项目中建议使用toml库) - mod_id_match = re.search(r'modId\s*=\s*"([^"]+)"', content) + # 从[[mods]]块中提取主modId(而不是从dependencies块中) + mod_id = self._extract_main_mod_id(content) + + # 提取modName(displayName或modId) + mod_name = self._extract_mod_name(content) + + # 提取版本号(从第一个version字段) version_match = re.search(r'version\s*=\s*"([^"]+)"', content) # 判断是Forge还是NeoForge loader = 'neoforge' if 'neoforge.mods.toml' in toml_path else 'forge' + # 优先级A: 读取dependencies中的side字段 (内部会先检查优先级B) + mod_type = self._infer_mod_type_from_forge(content) + mod_info = { - 'name': mod_id_match.group(1) if mod_id_match else '', + 'mod_id': mod_id, + 'mod_name': mod_name, 'version': version_match.group(1) if version_match else '', 'loader': loader, - 'type': self._infer_mod_type_from_forge(content) + 'type': mod_type } - self.logger.debug(f"解析{loader.upper()} Mod: {mod_info['name']}") + self.logger.debug(f"解析{loader.upper()} Mod: {mod_info['mod_id']} ({mod_info['mod_name']})") return mod_info except Exception as e: self.logger.error(f"解析mods.toml失败: {str(e)}") return None + def _extract_main_mod_id(self, content: str) -> str: + """ + 从TOML内容中提取主modId(从[[mods]]块中) + + Args: + content: TOML文件内容 + + Returns: + 主modId,如果未找到返回空字符串 + """ + # 查找[[mods]]块 + mods_pattern = r'\[\[mods\]\](.*?)(?=\[\[|$)' + mods_matches = re.findall(mods_pattern, content, re.DOTALL) + + # 从第一个[[mods]]块中提取modId + for mod_block in mods_matches: + mod_id_match = re.search(r'modId\s*=\s*"([^"]+)"', mod_block) + if mod_id_match: + return mod_id_match.group(1) + + # 如果没有找到[[mods]]块,尝试直接匹配(兼容旧格式) + mod_id_match = re.search(r'modId\s*=\s*"([^"]+)"', content) + return mod_id_match.group(1) if mod_id_match else '' + + def _extract_mod_name(self, content: str) -> str: + """ + 从TOML内容中提取mod名称 + + Args: + content: TOML文件内容 + + Returns: + mod名称,如果未找到则返回空字符串 + """ + # 优先使用displayName + display_name_match = re.search(r'displayName\s*=\s*"([^"]+)"', content) + if display_name_match: + return display_name_match.group(1) + + # 其次使用modId作为fallback + mod_id_match = re.search(r'modId\s*=\s*"([^"]+)"', content) + if mod_id_match: + return mod_id_match.group(1) + + return '' + def _parse_mcmod_info(self, zip_file: zipfile.ZipFile) -> Optional[Dict[str, Any]]: """ 解析旧版mcmod.info配置 @@ -147,19 +230,28 @@ def _parse_mcmod_info(self, zip_file: zipfile.ZipFile) -> Optional[Dict[str, Any """ try: with zip_file.open(self.MC_MOD_INFO) as f: - data = json.load(f) + data = json.loads(f.read().decode('utf-8')) # mcmod.info可能是数组或对象 if isinstance(data, list): data = data[0] if data else {} + mod_id = data.get('modid', '') + mod_name = data.get('name', '') + version = data.get('version', '') + + # Legacy配置没有明确的类型标识,使用关键词匹配 (内部会先检查优先级B) + mod_type = self._infer_mod_type_from_legacy(mod_id) + mod_info = { - 'name': data.get('modid', ''), - 'version': data.get('version', ''), - 'type': self._infer_mod_type_from_legacy(data) + 'mod_id': mod_id, + 'mod_name': mod_name, + 'version': version, + 'loader': 'forge', + 'type': mod_type } - self.logger.debug(f"解析Legacy Mod: {mod_info['name']}") + self.logger.debug(f"解析Legacy Mod: {mod_id} ({mod_name})") return mod_info except Exception as e: @@ -168,92 +260,176 @@ def _parse_mcmod_info(self, zip_file: zipfile.ZipFile) -> Optional[Dict[str, Any def _infer_mod_type_from_fabric(self, data: Dict) -> str: """ - 从Fabric配置推断Mod类型 + 从Fabric配置推断Mod类型(三层优先级) 判断逻辑: - - 检查depends和suggests字段 - - 如果有服务端相关依赖,可能是服务端Mod - - 如果只有客户端相关依赖,是客户端Mod + 1. 优先检查规则数据库(优先级A) + - 如果规则已确认(confirmed=true)→ 直接使用 + - 如果规则未确认 → 需要验证(降级到优先级B) + 2. 读取environment字段(优先级B) + 3. 使用Modrinth API检索(优先级C) + 4. 无法判断则返回unknown """ - depends = data.get('depends', {}) - suggests = data.get('suggests', {}) + mod_id = data.get('id', '') + mod_name = data.get('name', '') - # 常见的服务端API - server_apis = {'fabric-api', 'fabric', 'server'} - # 常见的客户端API - client_apis = {'fabric-renderer', 'cloth-config', 'modmenu'} + # 优先级A: 检查规则数据库(如果启用) + if not self.skip_rules: + rule = self.rule_manager.find_rule(mod_id) + if rule: + # 如果规则已确认,直接信任 + if rule.get('confirmed', False): + mod_type = rule.get('type') + self.logger.debug(f"[优先级A-已确认规则] {mod_id} -> {mod_type}") + return mod_type + else: + # 未确认的规则,需要验证(降级到优先级B) + self.logger.debug(f"[优先级A-未确认规则] {mod_id},将重新解析JAR验证") - has_server_dep = any(api in depends for api in server_apis) - has_client_dep = any(api in depends for api in client_apis) - - # 根据依赖关系推断类型 - if has_client_dep and not has_server_dep: + # 优先级B: 读取environment字段 + env = data.get('environment', '').lower() + if env == 'client': return 'client_only' - elif has_server_dep and not has_client_dep: - return 'client_optional_server_required' - else: - # 默认认为两端都需要 + elif env == 'server': + return 'server_only' + elif env == '*': return 'client_and_server_required' + + # 优先级C: 使用Modrinth API检索 + api_type = self.modrinth_api.classify_mod_via_api(mod_name, mod_id) + if api_type: + return api_type + + # 无法判断,返回unknown + return 'unknown' def _infer_mod_type_from_forge(self, content: str) -> str: """ - 从Forge/NeoForge配置推断Mod类型 + 从Forge/NeoForge配置推断Mod类型(三层优先级) 判断逻辑: - 1. 如果有明确的side字段,直接使用 - 2. 根据modId和描述关键词推断 - 3. 默认策略:客户端需装,服务端可选(更保守的选择) + 1. 优先检查规则数据库(优先级A) + - 如果规则已确认(confirmed=true)→ 直接使用 + - 如果规则未确认 → 需要验证(降级到优先级B) + 2. 检查核心依赖(minecraft/neoforge/forge/fabric)的side字段(优先级B-1) + - 如果核心依赖中有任何一个是CLIENT → client_only + - 如果核心依赖中有任何一个是SERVER → server_only + - 如果核心依赖全是BOTH → client_and_server_required + 3. 如果核心依赖无法判断(如缺失),再检查其他业务依赖(优先级B-2) + 4. 使用Modrinth API检索(优先级C) + 5. 无法判断则返回unknown """ - # 1. 查找side字段 - side_match = re.search(r'side\s*=\s*"(\w+)"', content) + # 提取modId用于规则匹配 + mod_id_match = re.search(r'modId\s*=\s*"([^"]+)"', content) + if not mod_id_match: + return 'unknown' + + mod_id = mod_id_match.group(1) + + # 提取modName用于辅助匹配 + mod_name = self._extract_mod_name(content) + + # 优先级A: 检查规则数据库(如果启用) + if not self.skip_rules: + rule = self.rule_manager.find_rule(mod_id) + if rule: + # 如果规则已确认,直接信任 + if rule.get('confirmed', False): + mod_type = rule.get('type') + self.logger.debug(f"[优先级A-已确认规则] {mod_id} -> {mod_type}") + return mod_type + else: + # 未确认的规则,需要验证(降级到优先级B) + self.logger.debug(f"[优先级A-未确认规则] {mod_id},将重新解析JAR验证") + + # 解析所有dependencies块 + # 注意: [[dependencies.XXX]]中的XXX是当前mod的ID,不是依赖的ID + # 需要从每个块内的modId字段获取真正的依赖ID + deps_pattern = r'\[\[dependencies\.[^\]]+\]\](.*?)(?=\[\[|$)' + deps_matches = re.findall(deps_pattern, content, re.DOTALL) + + # 分类依赖项 + core_deps = [] # 核心依赖: minecraft, neoforge, forge, fabric + other_deps = [] # 其他业务依赖 + + for dep_content in deps_matches: + # 从块内提取真正的依赖modId + dep_mod_id_match = re.search(r'modId\s*=\s*"([^"]+)"', dep_content) + side_match = re.search(r'side\s*=\s*"(\w+)"', dep_content) + + if dep_mod_id_match and side_match: + dep_mod_id = dep_mod_id_match.group(1).lower() + side = side_match.group(1).upper() + + # 判断是否为核心依赖 + if dep_mod_id in ['minecraft', 'neoforge', 'forge', 'fabric']: + core_deps.append(side) + else: + other_deps.append(side) - if side_match: - side = side_match.group(1).lower() - if side == 'client': + # 优先级B-1: 检查核心依赖的side字段 + if core_deps: + # 如果核心依赖中有任何一个是CLIENT + if any(side == 'CLIENT' for side in core_deps): return 'client_only' - elif side == 'server': - return 'client_optional_server_required' - else: + # 如果核心依赖中有任何一个是SERVER + elif any(side == 'SERVER' for side in core_deps): + return 'server_only' + # 如果核心依赖全是BOTH + elif all(side == 'BOTH' for side in core_deps): return 'client_and_server_required' + # 核心依赖混合情况(如既有BOTH又有其他),继续检查其他依赖 - # 2. 提取modId进行关键词匹配 - mod_id_match = re.search(r'modId\s*=\s*"([^"]+)"', content) - if mod_id_match: - mod_id = mod_id_match.group(1).lower() - - # 明显的客户端Mod关键词 - client_keywords = [ - 'jei', 'rei', 'emi', # 物品管理器 - 'journeymap', 'xaero', 'minimap', # 小地图 - 'appleskin', 'hud', 'overlay', # HUD覆盖层 - 'mouse', 'keybind', 'control', # 控制相关 - 'shader', 'optifine', 'iris', 'sodium', # 渲染优化 - 'dynamiccrosshair', 'crosshair', # 准星 - 'searchable', 'search', # 搜索功能 - ] - - # 明显的服务端Mod关键词 - server_keywords = [ - 'backup', 'performance', 'optimization', - 'world', 'chunk', 'generation', - ] - - # 检查是否包含客户端关键词 - if any(keyword in mod_id for keyword in client_keywords): - return 'client_required_server_optional' - - # 检查是否包含服务端关键词 - if any(keyword in mod_id for keyword in server_keywords): - return 'client_optional_server_required' + # 优先级B-2: 检查其他业务依赖的side字段 + if other_deps: + # 如果所有业务依赖都是CLIENT + if all(side == 'CLIENT' for side in other_deps): + return 'client_only' + # 如果所有业务依赖都是SERVER + elif all(side == 'SERVER' for side in other_deps): + return 'server_only' + # 如果所有业务依赖都是BOTH + elif all(side == 'BOTH' for side in other_deps): + return 'client_and_server_required' + # 混合情况,无法判断 + + # 优先级C: 使用Modrinth API检索 + api_type = self.modrinth_api.classify_mod_via_api(mod_name, mod_id) + if api_type: + return api_type - # 3. 默认策略:客户端需装,服务端可选 - # (比两端都需要更保守,避免不必要的服务端安装) - return 'client_required_server_optional' + # 无法判断,返回unknown + return 'unknown' - def _infer_mod_type_from_legacy(self, data: Dict) -> str: + def _infer_mod_type_from_legacy(self, mod_id: str) -> str: """ - 从旧版配置推断Mod类型 + 从Legacy配置推断Mod类型(三层优先级) + + 判断逻辑: + 1. 优先检查规则数据库(优先级A) + - 如果规则已确认(confirmed=true)→ 直接使用 + - 如果规则未确认 → 需要验证(降级到优先级C) + 2. 使用Modrinth API检索(优先级C) + 3. 无法判断则返回unknown """ - # 旧版配置通常没有明确的类型标识 - # 默认返回需要两端的类型 - return 'client_and_server_required' + # 优先级A: 检查规则数据库(如果启用) + if not self.skip_rules: + rule = self.rule_manager.find_rule(mod_id) + if rule: + # 如果规则已确认,直接信任 + if rule.get('confirmed', False): + mod_type = rule.get('type') + self.logger.debug(f"[优先级A-已确认规则] {mod_id} -> {mod_type}") + return mod_type + else: + # 未确认的规则,需要验证(降级到优先级C) + self.logger.debug(f"[优先级A-未确认规则] {mod_id},将使用API验证") + + # 优先级C: 使用Modrinth API检索 + api_type = self.modrinth_api.classify_mod_via_api('', mod_id) + if api_type: + return api_type + + # Legacy配置没有明确的类型标识,返回unknown + return 'unknown' + diff --git a/src/python/main.py b/src/python/main.py index d0f0bc9..a72c626 100644 --- a/src/python/main.py +++ b/src/python/main.py @@ -6,11 +6,12 @@ """ import sys -import os +import json from pathlib import Path from mod_classifier import ModClassifier from logger import setup_logger from i18n import i18n +from github_integration import GitHubIntegration def main(): @@ -49,6 +50,63 @@ def main(): logger.info(i18n.get('completed')) print(f"\n[OK] {i18n.get('completed')}") + # 询问是否创建GitHub Issue(可选功能) + if classifier.stats['auto_detected'] > 0: + github = GitHubIntegration() + if github.repo_info: + print("\n" + "="*60) + print("📋 GitHub Issue 报告(可选)") + print("="*60) + print(f"本次分类新增了 {classifier.stats['auto_detected']} 个Mod") + print("可以创建一个GitHub Issue来记录这些新分类。") + print("注意:这需要GitHub账号和Personal Access Token\n") + + choice = input("是否创建GitHub Issue? (y/n): ").strip().lower() + if choice in ['y', 'yes', '是']: + # 读取最近生成的增量补丁文件 + import glob + patch_files = glob.glob('rule_update_patch_*.json') + + if patch_files: + # 使用最新的补丁文件 + latest_patch = max(patch_files, key=lambda x: x) + try: + with open(latest_patch, 'r', encoding='utf-8') as f: + patch_data = json.load(f) + + # 从补丁中提取新增的Mod + new_mods_from_patch = patch_data.get('new_rules', []) + + if new_mods_from_patch: + title, body = github.generate_issue_content(new_mods_from_patch) + success = github.create_github_issue( + title, + body, + labels=['automation', 'mod-classification'] + ) + if not success: + # 如果API提交失败,提供保存文件的选项 + print("\n💡 提示: 您可以选择将Issue内容保存为文件,然后手动提交") + save_choice = input("是否保存Issue内容为Markdown文件? (y/n): ").strip().lower() + if save_choice in ['y', 'yes', '是']: + github.save_issue_to_file(title, body) + else: + print("\n补丁中没有新增Mod,无需创建Issue") + except Exception as e: + print(f"\n⚠️ 读取补丁文件失败: {str(e)}") + print(" 将使用自动检测的Mod列表") + # 降级方案:使用原来的方法 + new_mods = classifier.config_manager.mods_data[-classifier.stats['auto_detected']:] + if new_mods: + title, body = github.generate_issue_content(new_mods) + github.save_issue_to_file(title, body) + else: + print("\n未找到增量补丁文件,无法生成Issue报告") + else: + print("\n已跳过GitHub Issue创建") + else: + print("\n💡 提示: 如果配置了Git远程仓库,可以自动创建GitHub Issue报告新分类的Mod") + except Exception as e: logger.error(f"{i18n.get('error')}: {str(e)}", exc_info=True) print(f"\n[ERROR] {i18n.get('error')}: {str(e)}") diff --git a/src/python/mod_classifier.py b/src/python/mod_classifier.py index 726ad1c..6aeb2df 100644 --- a/src/python/mod_classifier.py +++ b/src/python/mod_classifier.py @@ -11,7 +11,7 @@ from logger import setup_logger from config_manager import ConfigManager from jar_parser import JarParser -from file_utils import clean_mod_name, ensure_directory, get_jar_files +from file_utils import ensure_directory, get_jar_files from i18n import i18n @@ -107,6 +107,13 @@ def classify_mods(self): self.logger.info(f"检测到 {self.stats['auto_detected']} 个新Mod,保存配置...") self.config_manager.save_config() + # 在同步规则之前生成补丁(如果有变更) + if self.stats['auto_detected'] > 0: + self._generate_patch_before_sync() + + # 将配置同步至规则数据库(总是执行,以更新reason字段) + self._sync_new_mods_to_rules() + # 输出统计信息 self._print_statistics() @@ -118,43 +125,82 @@ def _process_jar_file(self, jar_path: Path): jar_path: JAR文件路径 """ filename = jar_path.name - clean_name = clean_mod_name(filename) self.logger.info(f"\n处理: {filename}") - self.logger.debug(f"清理后的名称: {clean_name}") - # 1. 在配置中查找(暂不传版本和loader,后续可扩展) - mod_config = self.config_manager.find_mod(clean_name) + # 1. 解析JAR文件获取modId和类型(三层优先级判断) + mod_info = self.jar_parser.parse_jar(jar_path) + + if not mod_info: + # 无法解析JAR,尝试从文件名推断或直接分到Unknown + self.logger.warning(f"无法解析 {filename},尝试使用规则数据库或API") + # 使用空mod_id,让后续逻辑处理 + mod_id = '' + mod_name = '' + mod_type = 'unknown' + else: + mod_id = mod_info.get('mod_id', '') + mod_name = mod_info.get('mod_name', '') + mod_type = mod_info.get('type', 'unknown') + + if not mod_id: + self.logger.warning(f"{filename} 中未找到modId,尝试使用规则数据库或API") + mod_type = 'unknown' + + self.logger.debug(f"Mod ID: {mod_id}, Mod Name: {mod_name}, 推断类型: {mod_type}") + + # 2. 如果mod_id为空且类型为unknown,直接分到Unknown + if not mod_id and mod_type == 'unknown': + self.logger.info(f"[!] 无法识别的Mod,分类到: Unknown") + self._copy_to_output(jar_path, 'unknown') + self.stats['failed'] += 1 + return + + # 3. 在配置中查找(仅基于mod_id) + mod_config = self.config_manager.find_mod(mod_id) if mod_config: # 配置中存在,直接使用 mod_type = mod_config['type'] self.logger.info(f"[OK] 在配置中找到: {mod_type}") else: - # 2. 配置中不存在,解析JAR文件 - self.logger.info("配置中未找到,尝试解析JAR文件...") - mod_info = self.jar_parser.parse_jar(jar_path) - - if mod_info: - mod_type = mod_info['type'] - version = mod_info.get('version', '') - loader = mod_info.get('loader', '') - + # 4. 检查 mod_rules.json 中是否有已确认的规则 + from rule_manager import RuleManager + rule_manager = RuleManager() + if rule_manager.load_rules(): + confirmed_rule = rule_manager.find_rule(mod_id) + if confirmed_rule and confirmed_rule.get('confirmed', False): + # 使用已确认的规则 + mod_type = confirmed_rule['type'] + self.logger.info(f"[OK] 使用已确认规则: {mod_type}") + # 添加到 mods_data.json 以便后续快速查询 + self.config_manager.add_mod(mod_id, mod_type, mod_name) + else: + # 5. 配置和规则中都不存在,使用解析结果中的类型 + # 如果类型是unknown,不保存到配置,直接分到Unknown + if mod_type == 'unknown': + self.logger.info(f"[!] 无法确定类型,分类到: Unknown") + self._copy_to_output(jar_path, 'unknown') + self.stats['failed'] += 1 + return + + self.logger.info(f"[OK] 自动检测到类型: {mod_type}") + # 添加到配置中(包含mod_id、mod_name和type) + if self.config_manager.add_mod(mod_id, mod_type, mod_name): + self.stats['auto_detected'] += 1 + else: + # 无法加载规则数据库,使用解析结果 + if mod_type == 'unknown': + self.logger.info(f"[!] 无法确定类型,分类到: Unknown") + self._copy_to_output(jar_path, 'unknown') + self.stats['failed'] += 1 + return + self.logger.info(f"[OK] 自动检测到类型: {mod_type}") - if version: - self.logger.debug(f" 版本: {version}") - if loader: - self.logger.debug(f" Mod端: {loader.upper()}") - - # 添加到配置中(包含版本和loader信息) - if self.config_manager.add_mod(clean_name, mod_type, version, loader): + if self.config_manager.add_mod(mod_id, mod_type, mod_name): self.stats['auto_detected'] += 1 - else: - # 3. 解析失败,标记为未知 - self.logger.warning("[FAIL] 无法解析JAR文件,标记为未知类型") - mod_type = 'unknown' - # 4. 复制文件到对应目录 + # 5. 复制文件到对应目录 self._copy_to_output(jar_path, mod_type) def _copy_to_output(self, source_path: Path, mod_type: str): @@ -170,8 +216,8 @@ def _copy_to_output(self, source_path: Path, mod_type: str): # 检查目标文件是否已存在 if target_path.exists(): - self.logger.info(f"⊘ 文件已存在(已分类): {target_dir_name}") - self.stats['classified'] += 1 # 计入已分类,而不是跳过 + self.logger.info(f"⊙ 文件已存在(已分类): {target_dir_name}") + self.stats['classified'] += 1 return try: @@ -194,3 +240,115 @@ def _print_statistics(self): self.logger.info(f"自动检测新Mod: {self.stats['auto_detected']}") self.logger.info(f"{i18n.get('total_failed').format(self.stats['failed'])}") self.logger.info("=" * 60) + + def _generate_patch_before_sync(self): + """ + 在同步规则之前生成增量补丁 + 此时 mods_data.json 已更新,但 mod_rules.json 还未同步,可以检测到差异 + """ + from generate_patch import generate_incremental_patch + + print("\n" + "="*60) + print("🔄 正在生成规则更新补丁...") + print("="*60) + self.logger.info("\n正在生成规则更新补丁...") + + try: + patch_file = generate_incremental_patch() + + if patch_file: + print(f"\n✅ 增量补丁文件已生成: {patch_file}") + print(f" 提交方式:") + print(f" 1. 通过GitHub Issue提交补丁内容") + print(f" 2. 维护者使用 apply_patch.py 自动合并") + self.logger.info(f"✓ 增量补丁文件已生成: {patch_file}") + self.logger.info(f" 提交方式:") + self.logger.info(f" 1. 通过GitHub Issue提交补丁内容") + self.logger.info(f" 2. 维护者使用 apply_patch.py 自动合并") + else: + print("\nℹ️ 规则数据库已是最新,无需生成补丁") + self.logger.info("规则数据库已是最新,无需生成补丁") + except Exception as e: + print(f"\n⚠️ 补丁生成失败: {str(e)}") + self.logger.error(f"补丁生成失败: {str(e)}", exc_info=True) + + def _sync_new_mods_to_rules(self): + """ + 将新识别的Mod从mods_data.json转正至mod_rules.json + 如果规则已存在则更新字段,否则新增 + """ + from rule_manager import RuleManager + + # 加载规则数据库 + rule_manager = RuleManager() + if not rule_manager.load_rules(): + self.logger.warning("无法加载规则数据库,跳过转正") + return + + synced_count = 0 + updated_count = 0 + + # 遍历所有配置中的Mod + for mod_config in self.config_manager.mods_data: + mod_id = mod_config.get('mod_id', '') + mod_name = mod_config.get('mod_name', '') + mod_type = mod_config.get('type', 'unknown') + + if not mod_id: + continue + + # 跳过unknown类型的mod,不将其同步到规则库 + if mod_type == 'unknown': + self.logger.debug(f" 跳过(unknown类型): {mod_id}") + continue + + # 检查规则数据库中是否已存在 + existing_rule = rule_manager.find_rule(mod_id) + + if not existing_rule: + # 新增规则 + new_rule = { + 'mod_id': mod_id, + 'mod_name': mod_name, + 'type': mod_type, + 'reason': f'通过JAR配置自动识别: {mod_name}' + } + rule_manager.rules.append(new_rule) + synced_count += 1 + self.logger.debug(f" 新增: {mod_id} ({mod_name}) -> {mod_type}") + else: + # 检查是否为已确认配置,如果是则跳过更新 + is_confirmed = existing_rule.get('confirmed', False) + + if is_confirmed: + self.logger.debug(f" 跳过(已确认): {mod_id}") + continue + + # 更新现有规则的字段 + old_type = existing_rule.get('type', '') + old_name = existing_rule.get('mod_name', '') + + # 更新mod_name(如果为空或不同) + if mod_name and (not old_name or old_name != mod_name): + existing_rule['mod_name'] = mod_name + + # 更新type(如果不同) + if mod_type and old_type != mod_type: + existing_rule['type'] = mod_type + self.logger.debug(f" 更新类型: {mod_id} {old_type} -> {mod_type}") + + # 更新reason(如果为空) + if not existing_rule.get('reason'): + existing_rule['reason'] = f'通过JAR配置自动识别: {mod_name}' + + updated_count += 1 + + # 保存规则数据库 + total_count = synced_count + updated_count + if total_count > 0: + if rule_manager.save_rules(): + self.logger.info(f"✓ 成功同步 {total_count} 个Mod至规则数据库 (新增: {synced_count}, 更新: {updated_count})") + else: + self.logger.error("✗ 保存规则数据库失败") + else: + self.logger.info("没有需要同步的Mod") diff --git a/src/python/modrinth_api.py b/src/python/modrinth_api.py new file mode 100644 index 0000000..7682ecc --- /dev/null +++ b/src/python/modrinth_api.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Modrinth API集成模块 +通过Modrinth API查询Mod信息以辅助分类 + +优先级C: 规则数据库 > 配置文件标识 > Modrinth API检索 +""" + +import json +import re +from pathlib import Path +from typing import Optional, Dict, Any +from urllib.request import urlopen, Request +from urllib.error import URLError, HTTPError +from logger import setup_logger + +logger = setup_logger() + + +class ModrinthAPI: + """Modrinth API客户端""" + + BASE_URL = "https://api.modrinth.com/v2" + + def __init__(self): + self.logger = logger + + def search_project(self, query: str) -> Optional[Dict[str, Any]]: + """ + 搜索Modrinth项目 + + Args: + query: 搜索关键词(mod_id或mod_name) + + Returns: + 项目信息字典,如果未找到返回None + """ + try: + # URL编码查询参数 + encoded_query = query.replace(' ', '%20') + url = f"{self.BASE_URL}/search?query={encoded_query}&limit=1" + + self.logger.debug(f"[Modrinth API] 搜索: {query}") + + # 发送请求 + req = Request(url) + req.add_header('User-Agent', 'Minecraft-Mod-Classifier/2.0.0') + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + + if data.get('hits') and len(data['hits']) > 0: + project = data['hits'][0] + self.logger.debug(f"[Modrinth API] 找到匹配: {project.get('title', 'Unknown')}") + return project + else: + self.logger.debug(f"[Modrinth API] 未找到匹配: {query}") + return None + + except HTTPError as e: + self.logger.warning(f"[Modrinth API] HTTP错误 {e.code}: {query}") + return None + except URLError as e: + self.logger.warning(f"[Modrinth API] 网络错误: {str(e)}") + return None + except Exception as e: + self.logger.error(f"[Modrinth API] 搜索失败: {str(e)}") + return None + + def get_project_by_id(self, project_id: str) -> Optional[Dict[str, Any]]: + """ + 通过项目ID获取详细信息 + + Args: + project_id: Modrinth项目ID + + Returns: + 项目详细信息字典,如果未找到返回None + """ + try: + url = f"{self.BASE_URL}/project/{project_id}" + + self.logger.debug(f"[Modrinth API] 获取项目详情: {project_id}") + + req = Request(url) + req.add_header('User-Agent', 'Minecraft-Mod-Classifier/2.0.0') + + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + return data + + except HTTPError as e: + self.logger.warning(f"[Modrinth API] HTTP错误 {e.code}: {project_id}") + return None + except URLError as e: + self.logger.warning(f"[Modrinth API] 网络错误: {str(e)}") + return None + except Exception as e: + self.logger.error(f"[Modrinth API] 获取项目详情失败: {str(e)}") + return None + + def infer_type_from_categories(self, categories: list) -> Optional[str]: + """ + 根据Modrinth的分类标签推断Mod类型 + + Args: + categories: 分类标签列表 + + Returns: + 推断的Mod类型,如果无法判断返回None + """ + if not categories: + return None + + # 客户端专用标签(仅影响渲染/UI,不影响游戏逻辑) + client_only_tags = [ + 'optimization', # 性能优化通常是客户端的(如Sodium) + ] + + # 服务端专用标签 + server_only_tags = [ + 'server-utility', 'administration' + ] + + # 双端需要的标签(库、世界生成、内容添加等) + both_sides_tags = [ + 'library', # 库文件通常双端都需要 + 'worldgen', # 世界生成需要服务端决定,客户端同步 + 'adventure', # 冒险内容需要双端同步 + 'storage', # 存储系统需要双端同步 + 'technology', # 科技类模组需要双端同步 + 'magic', # 魔法类模组需要双端同步 + 'equipment', # 装备类需要双端同步 + 'food', # 食物类需要双端同步 + ] + + # 检查是否包含各类标签 + has_client_only = any(tag in categories for tag in client_only_tags) + has_server_only = any(tag in categories for tag in server_only_tags) + has_both_sides = any(tag in categories for tag in both_sides_tags) + + # 优先判断:如果有明确的单端标签且没有双端标签 + if has_client_only and not has_both_sides and not has_server_only: + return 'client_only' + + if has_server_only and not has_both_sides and not has_client_only: + return 'server_only' + + # 如果有双端需要的标签,优先返回双端必装 + if has_both_sides: + return 'client_and_server_required' + + # 如果同时有客户端和服务端标签 + if has_client_only and has_server_only: + return 'client_and_server_required' + + # 默认情况下,大多数功能性mod需要双端同步 + # 除非明确标记为纯客户端优化 + if not has_client_only: + return 'client_and_server_required' + + return None + + def normalize_search_term(self, term: str) -> str: + """ + 标准化搜索词:删除下划线、转换为小写、拆分驼峰命名 + + Args: + term: 原始搜索词 + + Returns: + 标准化后的搜索词 + """ + # 转换为小写 + normalized = term.lower() + + # 删除下划线和连字符,用空格替换 + normalized = re.sub(r'[_\-]', ' ', normalized) + + # 在驼峰命名处插入空格(大写字母前) + normalized = re.sub(r'([a-z])([A-Z])', r'\1 \2', normalized) + + # 去除多余空格 + normalized = ' '.join(normalized.split()) + + return normalized + + def search_mod_with_fallback(self, mod_name: str, mod_id: str) -> Optional[Dict[str, Any]]: + """ + 使用mod_name优先搜索,失败则使用mod_id + + Args: + mod_name: Mod名称 + mod_id: Mod ID + + Returns: + 项目信息字典,如果都未找到返回None + """ + result = None + + # 优先使用mod_name搜索 + if mod_name: + # 标准化搜索词 + normalized_name = self.normalize_search_term(mod_name) + self.logger.info(f"[Modrinth API] 使用mod_name搜索: {mod_name} -> {normalized_name}") + result = self.search_project(normalized_name) + + if result: + return result + + # 如果mod_name搜索失败,使用mod_id + if mod_id: + normalized_id = self.normalize_search_term(mod_id) + self.logger.info(f"[Modrinth API] 使用mod_id搜索: {mod_id} -> {normalized_id}") + result = self.search_project(normalized_id) + + if result: + return result + + return None + + def classify_mod_via_api(self, mod_name: str, mod_id: str) -> Optional[str]: + """ + 通过Modrinth API分类Mod + + Args: + mod_name: Mod名称 + mod_id: Mod ID + + Returns: + 分类类型,如果无法判断返回None + """ + # 搜索项目 + project = self.search_mod_with_fallback(mod_name, mod_id) + + if not project: + self.logger.debug(f"[Modrinth API] 无法通过API分类: {mod_name or mod_id}") + return None + + # 直接使用API返回的client_side和server_side字段 + # Modrinth side字段含义: + # - required: 必须安装 + # - optional: 可选安装(建议安装) + # - unsupported: 不支持/不应该安装 + client_side = project.get('client_side', '').lower() + server_side = project.get('server_side', '').lower() + + self.logger.info(f"[Modrinth API] client_side={client_side}, server_side={server_side}") + + # 如果任一环境标记为unsupported,说明是单端Mod + if client_side == 'unsupported' and server_side in ['required', 'optional']: + return 'server_only' + elif server_side == 'unsupported' and client_side in ['required', 'optional']: + return 'client_only' + + # 双端都支持的情况 + if client_side == 'required' and server_side == 'required': + return 'client_and_server_required' + elif client_side == 'required' and server_side == 'optional': + return 'client_required_server_optional' + elif client_side == 'optional' and server_side == 'required': + return 'client_optional_server_required' + elif client_side == 'optional' and server_side == 'optional': + # 两端都是optional,需要进一步判断 + categories = project.get('categories', []) + description = project.get('description', '').lower() + title = project.get('title', '').lower() + + # 纯客户端渲染/着色器类(明确只影响客户端视觉效果) + shader_cats = ['shader', 'resource-pack'] + if any(cat in categories for cat in shader_cats): + self.logger.info(f"[Modrinth API] 检测到着色器/资源包类别,归类为client_only") + return 'client_only' + + # 渲染优化类:有optimization类别且描述涉及渲染/视野/FPS等 + render_keywords = ['render', 'fps', 'graphics', 'distance', 'view', 'farther', + 'slide show', 'lag', 'performance', 'optimize'] + if 'optimization' in categories and any(kw in description for kw in render_keywords): + self.logger.info(f"[Modrinth API] 检测到渲染优化,归类为client_required_server_optional") + return 'client_required_server_optional' + + # optional/optional表示两端都可选安装,非必需 + return 'client_optional_server_optional' + + # 如果API没有返回明确的side信息,尝试从categories推断(后备方案) + self.logger.warning(f"[Modrinth API] 未找到side信息,使用categories推断") + categories = project.get('categories', []) + inferred_type = self.infer_type_from_categories(categories) + + if inferred_type: + self.logger.info(f"[Modrinth API] 基于分类推断类型: {inferred_type}") + return inferred_type + + # 最后的后备:从描述中推断 + description = project.get('description', '').lower() + + client_keywords = ['client-side', 'client only', 'rendering', 'hud', 'minimap', + 'shader', 'optifine', 'sodium', 'iris', 'gui', 'ui'] + server_keywords = ['server-side', 'server only', 'performance', 'optimization', + 'backup', 'admin', 'management'] + + has_client = any(keyword in description for keyword in client_keywords) + has_server = any(keyword in description for keyword in server_keywords) + + if has_client and not has_server: + return 'client_only' + elif has_server and not has_client: + return 'server_only' + elif has_client and has_server: + return 'client_and_server_required' + + self.logger.debug(f"[Modrinth API] 无法从API结果推断类型") + return None diff --git a/src/python/rule_manager.py b/src/python/rule_manager.py new file mode 100644 index 0000000..7fc2133 --- /dev/null +++ b/src/python/rule_manager.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +规则管理模块 +负责加载和管理mod_rules.json规则数据库 + +优先级顺序(带验证机制): +A. JAR配置文件标识 (side/environment字段) - 最高优先级 +B. 规则数据库 (mod_rules.json) - 中等优先级 + - 如果规则的reason来自JAR配置 → 直接使用 + - 如果规则的reason来自API/手动 → 需要重新解析JAR验证(降级到优先级A) +C. Modrinth API检索 - 最低优先级 + +补充设定: +有些模组API返回的信息可能与实际不一致,因此添加保底措施: +规则配置中"reason"非读取JAR配置文件得到的需要重新读取JAR进行验证。 +""" + +import json +import shutil +import re +from pathlib import Path +from typing import List, Dict, Optional +from logger import setup_logger +from file_utils import get_resource_path + +logger = setup_logger() + + +class RuleManager: + """规则管理器""" + + def __init__(self, rules_path: str = "config/mod_rules.json"): + """ + 初始化规则管理器 + + Args: + rules_path: 规则文件路径 + """ + # 使用 get_resource_path 获取正确的文件路径 + self.rules_path = get_resource_path(rules_path) + self.rules: List[Dict] = [] + self.logger = logger + + # 如果是打包环境且规则文件不存在,尝试从 _internal 复制初始规则 + import sys + if getattr(sys, 'frozen', False) and not self.rules_path.exists(): + internal_rules = Path(sys.executable).parent / '_internal' / rules_path + if internal_rules.exists(): + try: + from file_utils import ensure_directory + ensure_directory(self.rules_path.parent) + shutil.copy2(internal_rules, self.rules_path) + self.logger.info(f"从 _internal 复制初始规则文件") + except Exception as e: + self.logger.warning(f"复制初始规则文件失败: {str(e)}") + + def load_rules(self) -> bool: + """ + 加载规则文件 + + Returns: + 是否成功加载 + """ + if not self.rules_path.exists(): + self.logger.warning(f"规则文件 {self.rules_path} 不存在,使用空规则集") + return False + + try: + with open(self.rules_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self.rules = data.get('rules', []) + self.logger.info(f"成功加载 {len(self.rules)} 条Mod分类规则") + return True + + except json.JSONDecodeError as e: + self.logger.error(f"规则文件JSON格式错误: {str(e)}") + return False + except Exception as e: + self.logger.error(f"加载规则文件失败: {str(e)}") + return False + + def find_rule(self, mod_id: str) -> Optional[Dict]: + """ + 查找Mod的分类规则(基于mod_id精确匹配) + + Args: + mod_id: Mod的唯一标识符(modId) + + Returns: + 规则字典,如果未找到返回None + """ + for rule in self.rules: + if rule.get('mod_id', '').lower() == mod_id.lower(): + return rule + + return None + + def find_rule_by_name(self, mod_name: str) -> Optional[Dict]: + """ + 通过mod_name查找规则(支持模糊匹配) + + Args: + mod_name: Mod名称 + + Returns: + 规则字典,如果未找到返回None + """ + if not mod_name: + return None + + # 标准化搜索词 + normalized_name = self._normalize_term(mod_name) + + for rule in self.rules: + rule_name = rule.get('mod_name', '') + if not rule_name: + continue + + # 标准化规则中的名称 + normalized_rule_name = self._normalize_term(rule_name) + + # 精确匹配 + if normalized_name == normalized_rule_name: + return rule + + # 包含匹配 + if normalized_name in normalized_rule_name or normalized_rule_name in normalized_name: + return rule + + return None + + def _normalize_term(self, term: str) -> str: + """ + 标准化术语:删除下划线、转换为小写、拆分驼峰命名 + + Args: + term: 原始术语 + + Returns: + 标准化后的术语 + """ + # 转换为小写 + normalized = term.lower() + + # 删除下划线和连字符,用空格替换 + normalized = re.sub(r'[_\-]', ' ', normalized) + + # 在驼峰命名处插入空格(大写字母前) + normalized = re.sub(r'([a-z])([A-Z])', r'\1 \2', normalized) + + # 去除多余空格 + normalized = ' '.join(normalized.split()) + + return normalized + + def get_mod_type(self, mod_id: str, mod_name: str = '') -> Optional[str]: + """ + 获取Mod的类型(基于规则数据库) + + Args: + mod_id: Mod的唯一标识符(modId) + mod_name: Mod名称(可选,用于辅助匹配) + + Returns: + Mod类型,如果未找到返回None + """ + # 优先使用mod_id精确匹配 + rule = self.find_rule(mod_id) + if rule: + mod_type = rule.get('type') + is_confirmed = rule.get('confirmed', False) + + if is_confirmed: + self.logger.debug(f"[规则匹配-已确认] {mod_id} -> {mod_type}") + else: + self.logger.debug(f"[规则匹配-未确认] {mod_id} -> {mod_type} [需要验证]") + + return mod_type + + # 如果mod_id未找到,尝试使用mod_name模糊匹配 + if mod_name: + rule = self.find_rule_by_name(mod_name) + if rule: + mod_type = rule.get('type') + matched_id = rule.get('mod_id', '') + is_confirmed = rule.get('confirmed', False) + + if is_confirmed: + self.logger.debug(f"[规则匹配-by-name-已确认] {mod_name} -> {mod_type} (匹配到: {matched_id})") + else: + self.logger.debug(f"[规则匹配-by-name-未确认] {mod_name} -> {mod_type} (匹配到: {matched_id}) [需要验证]") + + return mod_type + + return None + + def save_rules(self) -> bool: + """ + 保存规则文件 + + Returns: + 是否成功保存 + """ + try: + # 确保目录存在 + self.rules_path.parent.mkdir(parents=True, exist_ok=True) + + data = { + 'rules': self.rules, + 'total': len(self.rules), + 'last_updated': str(__import__('datetime').datetime.now()) + } + + with open(self.rules_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + self.logger.info(f"成功保存 {len(self.rules)} 条Mod分类规则到 {self.rules_path}") + return True + + except Exception as e: + self.logger.error(f"保存规则文件失败: {str(e)}") + return False diff --git a/src/python/update_mod_names.py b/src/python/update_mod_names.py new file mode 100644 index 0000000..a374f0f --- /dev/null +++ b/src/python/update_mod_names.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +规则数据库mod_name填充工具 +对mod_rules.json中mod_name为空的条目执行Modrinth API检索并更新 +""" + +import json +import time +from pathlib import Path +from typing import List, Dict +from logger import setup_logger +from modrinth_api import ModrinthAPI + +logger = setup_logger() + + +class ModNameUpdater: + """Mod名称更新器""" + + def __init__(self, rules_path: str = "config/mod_rules.json"): + """ + 初始化更新器 + + Args: + rules_path: 规则文件路径 + """ + self.rules_path = Path(rules_path) + self.modrinth_api = ModrinthAPI() + self.logger = logger + + def load_rules(self) -> Dict: + """加载规则文件""" + if not self.rules_path.exists(): + self.logger.error(f"规则文件不存在: {self.rules_path}") + return None + + try: + with open(self.rules_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self.logger.info(f"成功加载 {len(data.get('rules', []))} 条规则") + return data + + except Exception as e: + self.logger.error(f"加载规则文件失败: {str(e)}") + return None + + def save_rules(self, data: Dict) -> bool: + """保存规则文件""" + try: + with open(self.rules_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + self.logger.info(f"成功保存 {len(data.get('rules', []))} 条规则") + return True + + except Exception as e: + self.logger.error(f"保存规则文件失败: {str(e)}") + return False + + def update_mod_names(self, batch_size: int = 10, delay: float = 1.0) -> bool: + """ + 更新mod_name字段 + + Args: + batch_size: 每批处理的条目数 + delay: 每次API请求之间的延迟(秒),避免速率限制 + + Returns: + 是否成功 + """ + print("\n" + "="*60) + print("开始更新规则数据库中的mod_name字段") + print("="*60) + + # 加载规则 + data = self.load_rules() + if not data: + return False + + rules = data.get('rules', []) + + # 筛选出mod_name为空的条目 + empty_name_rules = [rule for rule in rules if not rule.get('mod_name', '')] + + if not empty_name_rules: + print("\n✅ 所有规则的mod_name字段都已填充,无需更新") + return True + + print(f"\n找到 {len(empty_name_rules)} 条需要更新的规则") + print(f"将分 {((len(empty_name_rules) - 1) // batch_size) + 1} 批处理\n") + + updated_count = 0 + failed_count = 0 + skipped_count = 0 + + # 分批处理 + for i in range(0, len(empty_name_rules), batch_size): + batch = empty_name_rules[i:i + batch_size] + batch_num = (i // batch_size) + 1 + total_batches = ((len(empty_name_rules) - 1) // batch_size) + 1 + + print(f"\n--- 处理第 {batch_num}/{total_batches} 批 ({len(batch)} 条) ---") + + for j, rule in enumerate(batch, 1): + mod_id = rule.get('mod_id', '') + current_index = i + j + total = len(empty_name_rules) + + print(f"[{current_index}/{total}] 处理: {mod_id}", end=" ... ") + + # 使用Modrinth API检索 + try: + project = self.modrinth_api.search_project(mod_id) + + if project: + mod_name = project.get('title', '') + if mod_name: + rule['mod_name'] = mod_name + print(f"✅ {mod_name}") + updated_count += 1 + else: + print("⚠️ 未找到名称") + skipped_count += 1 + else: + print("❌ API未返回结果") + failed_count += 1 + + except Exception as e: + print(f"❌ 错误: {str(e)}") + failed_count += 1 + + # 延迟以避免速率限制 + if j < len(batch): # 最后一项不需要延迟 + time.sleep(delay) + + # 批次间额外延迟 + if i + batch_size < len(empty_name_rules): + print(f"\n等待 {delay * 2} 秒后继续下一批...") + time.sleep(delay * 2) + + # 保存更新后的规则 + print("\n" + "="*60) + print("保存更新后的规则...") + + if self.save_rules(data): + print("\n" + "="*60) + print("更新完成!") + print("="*60) + print(f"成功更新: {updated_count}") + print(f"失败: {failed_count}") + print(f"跳过: {skipped_count}") + print("="*60) + return True + else: + print("\n❌ 保存失败") + return False + + +def main(): + """主函数""" + updater = ModNameUpdater() + + print("\n此工具将对 mod_rules.json 中 mod_name 为空的条目") + print("执行 Modrinth API 检索并填充 mod_name 字段。") + print("注意:这可能需要较长时间,取决于条目数量和网络状况。\n") + + choice = input("是否继续? (y/n): ").strip().lower() + if choice not in ['y', 'yes', '是']: + print("已取消") + return + + success = updater.update_mod_names(batch_size=10, delay=1.0) + + if success: + print("\n✅ mod_name更新成功!") + else: + print("\n❌ mod_name更新失败,请查看日志") + + input("\n按Enter键退出...") + + +if __name__ == "__main__": + main() diff --git a/src/python/update_types_via_api.py b/src/python/update_types_via_api.py new file mode 100644 index 0000000..ff86da1 --- /dev/null +++ b/src/python/update_types_via_api.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +使用Modrinth API重新分类规则数据库中的Mod +直接调用优先级C(API检索)更新type字段 +""" + +import json +import time +from pathlib import Path +from typing import Dict +from logger import setup_logger +from modrinth_api import ModrinthAPI + +logger = setup_logger() + + +class TypeUpdater: + """类型更新器 - 直接使用Modrinth API""" + + def __init__(self, rules_path: str = "config/mod_rules.json"): + self.rules_path = Path(rules_path) + self.modrinth_api = ModrinthAPI() + self.logger = logger + + def load_rules(self) -> Dict: + """加载规则文件""" + if not self.rules_path.exists(): + self.logger.error(f"规则文件不存在: {self.rules_path}") + return None + + try: + with open(self.rules_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self.logger.info(f"成功加载 {len(data.get('rules', []))} 条规则") + return data + + except Exception as e: + self.logger.error(f"加载规则文件失败: {str(e)}") + return None + + def save_rules(self, data: Dict) -> bool: + """保存规则文件""" + try: + with open(self.rules_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + self.logger.info(f"成功保存 {len(data.get('rules', []))} 条规则") + return True + + except Exception as e: + self.logger.error(f"保存规则文件失败: {str(e)}") + return False + + def update_types(self, batch_size: int = 10, delay: float = 1.0) -> bool: + """ + 使用Modrinth API更新type字段 + + Args: + batch_size: 每批处理的条目数 + delay: 每次API请求之间的延迟(秒) + + Returns: + 是否成功 + """ + print("\n" + "="*60) + print("使用Modrinth API重新分类规则数据库") + print("="*60) + + # 加载规则 + data = self.load_rules() + if not data: + return False + + rules = data.get('rules', []) + + # 筛选出mod_name不为空的条目(已有名称才能进行API检索) + named_rules = [rule for rule in rules if rule.get('mod_name', '')] + + if not named_rules: + print("\n⚠️ 没有mod_name字段的规则,无法进行API检索") + return False + + print(f"\n找到 {len(named_rules)} 条有mod_name的规则") + print(f"将分 {((len(named_rules) - 1) // batch_size) + 1} 批处理\n") + + updated_count = 0 + unchanged_count = 0 + failed_count = 0 + + # 分批处理 + for i in range(0, len(named_rules), batch_size): + batch = named_rules[i:i + batch_size] + batch_num = (i // batch_size) + 1 + total_batches = ((len(named_rules) - 1) // batch_size) + 1 + + print(f"\n--- 处理第 {batch_num}/{total_batches} 批 ({len(batch)} 条) ---") + + for j, rule in enumerate(batch, 1): + mod_id = rule.get('mod_id', '') + mod_name = rule.get('mod_name', '') + old_type = rule.get('type', 'unknown') + current_index = i + j + total = len(named_rules) + + print(f"[{current_index}/{total}] {mod_id}", end=" ... ") + + try: + # 直接使用Modrinth API分类(优先级C) + new_type = self.modrinth_api.classify_mod_via_api(mod_name, mod_id) + + if new_type: + if new_type != old_type: + rule['type'] = new_type + rule['reason'] = f"通过Modrinth API重新分类: {mod_name}" + print(f"✅ {old_type} → {new_type}") + updated_count += 1 + else: + print(f"⏭️ 保持不变 ({old_type})") + unchanged_count += 1 + else: + print(f"⚠️ API无法判断,保持原类型 ({old_type})") + failed_count += 1 + + except Exception as e: + print(f"❌ 错误: {str(e)}") + failed_count += 1 + + # 延迟以避免速率限制 + if j < len(batch): + time.sleep(delay) + + # 批次间额外延迟 + if i + batch_size < len(named_rules): + print(f"\n等待 {delay * 2} 秒后继续下一批...") + time.sleep(delay * 2) + + # 保存更新后的规则 + print("\n" + "="*60) + print("保存更新后的规则...") + + if self.save_rules(data): + print("\n" + "="*60) + print("更新完成!") + print("="*60) + print(f"类型变更: {updated_count}") + print(f"保持不变: {unchanged_count}") + print(f"无法判断: {failed_count}") + print("="*60) + return True + else: + print("\n❌ 保存失败") + return False + + +def main(): + """主函数""" + updater = TypeUpdater() + + print("\n此工具将使用 Modrinth API 重新分类 mod_rules.json 中的Mod") + print("注意:") + print(" 1. 仅处理已有 mod_name 的条目") + print(" 2. 直接调用API检索,不使用规则数据库或JAR配置") + print(" 3. 这可能需要较长时间,取决于条目数量和网络状况\n") + + choice = input("是否继续? (y/n): ").strip().lower() + if choice not in ['y', 'yes', '是']: + print("已取消") + return + + success = updater.update_types(batch_size=10, delay=1.0) + + if success: + print("\n✅ 类型更新成功!") + else: + print("\n❌ 类型更新失败,请查看日志") + + try: + input("\n按Enter键退出...") + except EOFError: + pass # 管道输入时跳过 + + +if __name__ == "__main__": + main() diff --git "a/\351\234\200\346\261\202.md" "b/\351\234\200\346\261\202.md" new file mode 100644 index 0000000..a04802d --- /dev/null +++ "b/\351\234\200\346\261\202.md" @@ -0,0 +1,31 @@ +### 按以下要求完善代码 + +#### 处理jar文件 +1. 解析jar文件,获取modId、modname和类型 +2. 在配置中查找(仅基于mod_id) +3. 配置中不存在,使用解析结果中的类型(来自三层优先级判断) +4. 复制文件到对应目录 + +#### 规则管理模块 +优先级顺序: JAR配置文件标识 > 规则数据库(带验证) > Modrinth API检索 +1. JAR配置文件标识 解析jar文件获取'mod_id'、'mod_name'、'type',无需'loader''version',保存至mods_data.json - 优先级A(最高) +2. 读取environment字段 标识为优先级A的一部分 +3. 规则数据库 标识为优先级B(中等) + - 如果规则的"reason"来自JAR配置 → 直接使用 + - 如果规则的"reason"来自API/手动 → 需要重新解析JAR进行验证(降级到优先级A) +4. 无法判断则优先使用'mod_name'在https://api.modrinth.com/v2/上获取其分类,如未查询到或无'mod_name'字段,则转用'mod_id'字段重新查询。 - 优先级C(最低) +补充:拆分字段,删除下划线,完善匹配规则,添加匹配算法 +5. 最终仍无法判断的放入未知 + +保底措施: +有些模组API返回的信息可能与实际不一致,因此添加验证机制: +规则配置中"reason"非读取JAR配置文件得到的需要重新读取JAR进行验证。 +验证完毕后分类,最后同步信息修改至规则配置。 + +#### 转正mods_data.json +1. 优化规则数据库,添加'mod_name'字段,暂时空置 +2. 在全部分类完成后将mods_data.json内容转入规则数据库 +3. 完善提交自动提交gitissue的方案,仅去除git commit/push功能 + +#### 验证以上功能 +