From 0c4fd38f941fa955738c2d2e361758059ad02935 Mon Sep 17 00:00:00 2001 From: xiaofanforfabric <1706079731@qq.com> Date: Mon, 1 Dec 2025 15:52:18 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E5=A8=A3=E8=AF=B2=E5=A7=9E=20V3.0?= =?UTF-8?q?=20Forge=20=E9=91=B7=EE=81=84=E5=A7=A9=E9=8D=96=E6=A0=A7?= =?UTF-8?q?=E7=95=AF=E7=BB=AF=E8=8D=A4=E7=B2=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 娣诲姞 autosaveforForge 妯″潡锛氶潻鍛芥€х殑鑷姩鍖栧畯浠g爜鎵ц绯荤粺 - 鏍稿績鍔熻兘锛? * 鑷畾涔夊畯璇█锛屾敮鎸佸嚱鏁般€佹潯浠惰鍙ャ€佸惊鐜? * 鏃犵紳闆嗘垚 Baritone 璺緞鏌ユ壘 * Web 绠$悊鐣岄潰锛堢鍙?8079锛? * 鏂囦欢鐩戝惉鑷姩閲嶈浇 * 鏅鸿兘鍛戒护鍐茬獊妫€娴? * 鍩轰簬娓告垙鏃堕棿鐨勫畾鏃朵换鍔?- 鍏朵粬鍔熻兘锛? * 鐘舵€佺洃鎺ф湇鍔″櫒锛堢鍙?8083锛? * 鑱婂ぉ鐩戞帶鏈嶅姟鍣紙绔彛 8081锛? * 鏈嶅姟鍣ㄤ俊鎭?API锛堢鍙?2000锛? * 鑷姩鐫$湢鎺у埗 * 姝讳骸鑷姩鍋滄浠诲姟 * 鎬ц兘浼樺寲 - 鏇存柊 README.md锛屾坊鍔?V3.0 鏇存柊璇存槑鍜岀ず渚嬩唬鐮?- 娣诲姞 FanMacrodoc.md 瀹忚瑷€瀹屾暣鏂囨。 --- README.md | 177 ++- autosaveforForge/FanMacrodoc.md | 603 +++++++++ autosaveforForge/build.gradle | 239 ++++ autosaveforForge/gradle.properties | 55 + autosaveforForge/gradlew | 251 ++++ autosaveforForge/gradlew.bat | 94 ++ autosaveforForge/settings.gradle | 15 + .../autosaveforforge/AutoSleepController.java | 230 ++++ .../autosaveforforge/Autosaveforforge.java | 163 +++ .../autosaveforforge/BaritoneTaskManager.java | 470 +++++++ .../autosaveforforge/ChatMonitorServer.java | 740 +++++++++++ .../com/xiaofan/autosaveforforge/Config.java | 82 ++ .../autosaveforforge/DeathHandler.java | 109 ++ .../autosaveforforge/DoCommandHandler.java | 179 +++ .../autosaveforforge/MacroExecutor.java | 1109 +++++++++++++++++ .../xiaofan/autosaveforforge/MacroParser.java | 804 ++++++++++++ .../autosaveforforge/MacroWebServer.java | 623 +++++++++ .../autosaveforforge/NotFanMacroFound.java | 12 + .../PerformanceController.java | 286 +++++ .../autosaveforforge/ServerInfoAPI.java | 1008 +++++++++++++++ .../autosaveforforge/StatusHttpServer.java | 715 +++++++++++ .../autosaveforforge/TitleScreenDetector.java | 55 + .../autosaveforforge/WorldTimeHUD.java | 78 ++ .../src/main/resources/META-INF/mods.toml | 63 + .../resources/autosaveforforge.mixins.json | 17 + .../src/main/resources/pack.mcmeta | 6 + 26 files changed, 8176 insertions(+), 7 deletions(-) create mode 100644 autosaveforForge/FanMacrodoc.md create mode 100644 autosaveforForge/build.gradle create mode 100644 autosaveforForge/gradle.properties create mode 100644 autosaveforForge/gradlew create mode 100644 autosaveforForge/gradlew.bat create mode 100644 autosaveforForge/settings.gradle create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/AutoSleepController.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Autosaveforforge.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/BaritoneTaskManager.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ChatMonitorServer.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Config.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DeathHandler.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DoCommandHandler.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroExecutor.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroParser.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroWebServer.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/NotFanMacroFound.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/PerformanceController.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ServerInfoAPI.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/StatusHttpServer.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/TitleScreenDetector.java create mode 100644 autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/WorldTimeHUD.java create mode 100644 autosaveforForge/src/main/resources/META-INF/mods.toml create mode 100644 autosaveforForge/src/main/resources/autosaveforforge.mixins.json create mode 100644 autosaveforForge/src/main/resources/pack.mcmeta diff --git a/README.md b/README.md index 907ff90..da2cb0d 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ ## 📋 项目概述 -本项目包含两个主要模块: +本项目包含三个主要模块: 1. **QQbot模块** - 基于NapCat的QQ机器人,提供多种群聊和私聊功能 2. **Fabric客户端模组** - Minecraft 1.21.5客户端模组,提供服务器信息API和消息监听 +3. **autosaveforForge模块** - Minecraft 1.20.1 Forge客户端模组,提供**革命性的自动化宏代码执行系统** 🚀 ## 🏗️ 项目结构 @@ -42,6 +43,19 @@ autosave/ │ │ └── ... # 其他客户端功能 │ └── main/ │ +├── autosaveforForge/ # Forge客户端模组(V3.0新增) +│ ├── src/main/java/com/xiaofan/autosaveforforge/ +│ │ ├── MacroExecutor.java # 宏执行器(核心) +│ │ ├── MacroParser.java # 宏解析器(核心) +│ │ ├── BaritoneTaskManager.java # 任务管理器(核心) +│ │ ├── MacroWebServer.java # 宏管理Web服务器(端口8079) +│ │ ├── StatusHttpServer.java # 状态服务器(端口8083) +│ │ ├── ChatMonitorServer.java # 聊天监控(端口8081) +│ │ ├── ServerInfoAPI.java # 服务器信息API(端口2000) +│ │ ├── DoCommandHandler.java # /do命令处理器 +│ │ └── ... # 其他功能模块 +│ └── FanMacrodoc.md # 宏语言完整文档 +│ └── SimpfunPassAPI/ # API服务模块 ``` @@ -110,14 +124,123 @@ autosave/ #### 客户端控制(端口8083) - Web界面控制客户端功能 +### 🎯 autosaveforForge模块功能(V3.0 革命性更新!) + +#### ⭐ 自动化宏代码执行系统(核心功能) + +**这是本项目的革命性创新!** 通过自定义宏语言,你可以编写简单的文本文件来自动执行复杂的游戏任务,无需编程知识! + +##### 🌟 核心特性 + +- **📝 自定义宏语言**:简单易学的文本语法,像写脚本一样编写自动化任务 +- **🔄 条件执行**:支持位置检查、时间检查、物品检查等智能条件判断 +- **⚙️ 函数系统**:支持函数定义和调用,代码复用更简单 +- **🎮 Baritone集成**:无缝集成Baritone路径查找,自动挖矿、探索、移动 +- **🌐 Web管理界面**:通过浏览器管理宏,启动/停止/查看状态 +- **📁 文件监听**:修改宏文件后自动重载,无需重启游戏 +- **🛡️ 冲突检测**:智能检测命令冲突,自动停止冲突的宏 +- **⏰ 时间控制**:支持基于游戏时间的定时任务 + +##### 📚 宏语言文档 + +完整的宏语言语法和使用说明请查看:[**FanMacrodoc.md**](autosaveforForge/FanMacrodoc.md) + +##### 🎬 示例视频 + +> 📹 **演示视频**:[点击观看自动化宏系统演示](https://example.com/video) *(请替换为实际视频链接)* + +##### 💡 示例代码 + +以下是在演示视频中使用的三个宏文件: + +**1. 回家宏** (`回家.txt`) +```fanmacro +fan_main: + do fun "sleep" + +fun name="sleep"; + if time >= 11000 + do #stop; + do #goto -23 77 -3073 + do #set allowBreak false; + do #set allowPlace false; + do #goto 0 85 -3062; + do /home; + do #set allowBreak true; + do #set allowPlace true; + do end; + else + do wait 1m; + do fun "sleep"; +``` + +**2. 挖矿宏** (`挖矿.txt`) +```fanmacro +fan_main: + do fun "goto" + +fun name="goto"; + if me at = (0,85,-3062) + do #set allowBreak false; + do #set allowPlace false; + do #goto 19 78 -3016; + do #set allowBreak true; + do #set allowPlace true; + check me nothave (item = coal), do #mine minecraft:coal_ore; + check me nothave (item = raw_iron), do #mine minecraft:iron_ore; + check me nothave (item = diamond), do #mine minecraft:diamond_ore minecraft:deepslate_diamond_ore; + do wait 5m; + do #stop; + run name = "立即回家" + do end; + else + do #stop; + do /home; + do fun "goto"; +``` + +**3. 立即回家宏** (`立即回家.txt`) +```fanmacro +fan_main: + do #stop; + do #goto -23 77 -3073 + do #set allowBreak false; + do #set allowPlace false; + do #goto 0 85 -3062; + do /home; + do #set allowBreak true; + do #set allowPlace true; + do end; +``` + +##### 🚀 快速开始 + +1. **安装模组**:将模组放入 `.minecraft/mods/` 目录 +2. **创建宏文件**:在 `.minecraft/config/do/` 目录创建 `.txt` 文件 +3. **编写宏代码**:使用宏语言编写自动化任务 +4. **启动宏**: + - 游戏内:使用 `/do <宏名>` 命令 + - Web界面:访问 `http://localhost:8079` + +#### 其他功能 + +- **状态监控服务器**(端口8083):查看游戏状态,控制服务器连接 +- **聊天监控服务器**(端口8081):实时监控游戏聊天消息 +- **服务器信息API**(端口2000):提供服务器信息查询接口 +- **自动睡眠控制**:智能控制游戏内睡眠 +- **死亡处理**:玩家死亡时自动停止所有任务 +- **性能优化**:禁用窗口失去焦点时暂停,保持后台运行 + ## 🚀 快速开始 ### 环境要求 -- Java 21+ -- MySQL 8.0+ -- NapCat QQ机器人框架 -- Minecraft 1.21.5 + Fabric Loader 0.17.3 +- Java 17+ (Forge模组) / Java 21+ (Fabric模组和QQbot) +- MySQL 8.0+ (仅QQbot需要) +- NapCat QQ机器人框架 (仅QQbot需要) +- Minecraft 1.20.1 + Forge 47.4.12 (autosaveforForge模组) +- Minecraft 1.21.5 + Fabric Loader 0.17.3 (Fabric模组) +- Baritone Mod (autosaveforForge模组需要) ### 配置步骤 @@ -225,6 +348,14 @@ java -jar build/libs/qqbot-1.0-SNAPSHOT.jar ./gradlew build ``` +**编译Forge模组:** +```bash +cd autosaveforForge +./gradlew build +``` + +编译后的模组文件位于:`autosaveforForge/build/libs/autosaveforforge-1.0-SNAPSHOT.jar` + ## 📖 使用说明 ### QQ机器人命令列表 @@ -249,11 +380,13 @@ java -jar build/libs/qqbot-1.0-SNAPSHOT.jar | 端口 | 模块 | 功能 | |------|------|------| -| 2000 | Fabric客户端 | 服务器信息API | +| 2000 | Fabric客户端 / Forge客户端 | 服务器信息API | | 2001 | QQbot模块 | 绑定验证码API | | 3000 | NapCat | HTTP API | | 3001 | NapCat | WebSocket | -| 8083 | Fabric客户端 | 客户端控制 | +| 8079 | Forge客户端 | 宏管理Web界面 | +| 8081 | Forge客户端 | 聊天监控Web界面 | +| 8083 | Fabric客户端 / Forge客户端 | 客户端控制 / 状态监控 | ## 🔧 配置说明 @@ -323,6 +456,21 @@ QQ号2; - `mes.java` - 服务器消息API(端口2000) - `ClientControlServer.java` - 客户端控制(端口8083) +### autosaveforForge模块核心文件 + +- `MacroExecutor.java` - 宏执行器(核心,执行宏命令) +- `MacroParser.java` - 宏解析器(核心,解析宏文件) +- `BaritoneTaskManager.java` - 任务管理器(核心,管理宏生命周期) +- `MacroWebServer.java` - 宏管理Web服务器(端口8079) +- `StatusHttpServer.java` - 状态监控服务器(端口8083) +- `ChatMonitorServer.java` - 聊天监控服务器(端口8081) +- `ServerInfoAPI.java` - 服务器信息API(端口2000) +- `DoCommandHandler.java` - `/do`命令处理器 +- `DeathHandler.java` - 死亡处理(自动停止任务) +- `AutoSleepController.java` - 自动睡眠控制 +- `WorldTimeHUD.java` - 世界时间显示 +- `PerformanceController.java` - 性能优化控制 + ## 🔒 安全注意事项 1. **配置文件管理**: @@ -342,6 +490,21 @@ QQ号2; ## 📝 更新日志 +### V3.0版本(最新)🚀 +- **⭐ 革命性功能:自动化宏代码执行系统** + - 自定义宏语言,支持函数、条件语句、循环 + - 无缝集成Baritone路径查找 + - Web管理界面(端口8079) + - 文件监听自动重载 + - 智能命令冲突检测 + - 基于游戏时间的定时任务 +- 状态监控服务器(端口8083) +- 聊天监控服务器(端口8081) +- 服务器信息API(端口2000) +- 自动睡眠控制 +- 死亡自动停止任务 +- 性能优化(禁用窗口失去焦点暂停) + ### V1版本 - 基础触发词功能 - 签到系统 diff --git a/autosaveforForge/FanMacrodoc.md b/autosaveforForge/FanMacrodoc.md new file mode 100644 index 0000000..1d25a2b --- /dev/null +++ b/autosaveforForge/FanMacrodoc.md @@ -0,0 +1,603 @@ +# FanMacro 宏自动化工具文档 + +## 目录 + +1. [简介](#简介) +2. [宏文件格式](#宏文件格式) +3. [主入口点](#主入口点) +4. [函数定义](#函数定义) +5. [命令列表](#命令列表) +6. [条件语句](#条件语句) +7. [示例代码](#示例代码) +8. [注意事项](#注意事项) + +--- + +## 简介 + +FanMacro 是一个基于 Baritone API 的 Minecraft 自动化宏系统,允许玩家编写简单的文本文件来自动执行游戏中的各种任务。 + +### 宏文件位置 + +宏文件应放置在以下目录: +``` +.minecraft/config/do/ +``` + +宏文件必须是 `.txt` 格式,文件名即为宏名称。 + +### 注释 + +支持单行注释,使用 `//` 开头: +``` +do #goto 0 0 0; // 这是注释 +``` + +--- + +## 宏文件格式 + +### 基本结构 + +宏文件有两种格式: + +**格式1:无主入口点(传统格式)** +``` +do #goto 0 0 0; +do #mine iron_ore; +``` + +**格式2:带主入口点(推荐格式)** +``` +fun name="goto" type= &; + // 函数内容 + do #goto 0 0 0; + +fan_main: + do fun "goto"; + do #mine iron_ore; +``` + +--- + +## 主入口点 + +使用 `fan_main:` 作为宏的主入口点。当宏启动时,会从 `fan_main:` 之后的第一条命令开始执行。 + +**语法:** +``` +fan_main: + // 主程序命令 + do #goto 0 0 0; + do fun "myFunction"; +``` + +**特点:** +- `fan_main:` 之前可以定义函数 +- `fan_main:` 之后是主程序流程 +- 如果没有 `fan_main:`,则按顺序执行所有命令 + +--- + +## 函数定义 + +函数允许将代码块封装为可重用的单元。 + +### 语法 + +``` +fun name="函数名" type= &; + // 函数内容 + do #goto 0 0 0; + do #mine iron_ore; +``` + +### 参数说明 + +- `name="函数名"`:函数名称,必须用双引号包裹 +- `type= &`:可选参数 + - **不指定 `type`**:函数在主线程执行,会阻塞后续命令 + - **指定 `type= &`**:函数在后台执行,不阻塞主线程 + +### 调用函数 + +``` +do fun "函数名"; +``` + +### 示例 + +``` +fun name="gotoHome" type= &; + do #set allowBreak false; + do #set allowPlace false; + do #goto 0 85 -3062; + do #set allowBreak true; + do #set allowPlace true; + +fan_main: + do fun "gotoHome"; + do #mine iron_ore; +``` + +--- + +## 命令列表 + +### 1. do 命令 + +执行 Baritone 命令或原版 Minecraft 命令。 + +#### Baritone 命令(# 开头) + +**语法:** +``` +do #命令 参数; +``` + +**示例:** +``` +do #goto 0 0 0; // 移动到指定坐标 +do #mine iron_ore; // 挖掘铁矿石 +do #stop; // 停止当前任务 +do #set allowBreak true; // 设置允许破坏方块 +do #set allowPlace false; // 设置禁止放置方块 +``` + +**阻塞命令:** +以下命令会阻塞宏执行,直到命令完成: +- `#goto`:等待到达目标位置(容差3格) +- `#mine`:等待挖掘完成 +- `#explore`:等待探索完成 +- `#farm`:等待种植/收获完成 +- `#follow`:等待跟随完成 + +**非阻塞命令:** +- `#stop`:立即停止 +- `#set`:立即设置 + +#### 原版 Minecraft 命令(/ 开头) + +**语法:** +``` +do /命令 参数; +``` + +**示例:** +``` +do /home; // 执行 /home 命令 +do /spawn; // 执行 /spawn 命令 +do /tp 0 0 0; // 执行 /tp 命令 +``` + +--- + +### 2. check 命令 + +条件检查命令,如果条件满足则执行指定动作,否则继续执行下一个命令。 + +#### 语法1:检查物品(have) + +``` +check me have (item = 物品名, type = 工具类型, quantity = 数量), do 动作; +``` + +**参数说明:** +- `item`:物品名称(必需) + - 非工具物品:使用原版注册名(不加 `minecraft:` 前缀),如 `diamond`、`raw_iron` + - 工具物品:使用工具名称,如 `pickaxe`、`axe` +- `type`:工具类型(仅工具类物品,可选) + - 可选值:`diamond`、`iron`、`golden`、`stone`、`wooden`、`netherite` +- `quantity`:数量(可选,默认1,范围1-64) + +**示例:** +``` +check me have (item = diamond, quantity = 10), do #goto 0 0 0; +check me have (item = pickaxe, type = diamond, quantity = 1), do #mine iron_ore; +``` + +**注意:** +- 非工具物品使用精确匹配,`diamond` 不会匹配 `diamond_pickaxe` +- 工具物品可以部分匹配,`pickaxe` 可以匹配 `diamond_pickaxe` + +#### 语法2:检查没有物品(nothave) + +``` +check me nothave (item = 物品名), do 动作; +``` + +**参数说明:** +- `item`:物品名称(必需,仅支持此参数) +- 使用精确匹配,`diamond` 不会匹配 `diamond_pickaxe` + +**示例:** +``` +check me nothave (item = diamond), do #mine diamond_ore; +check me nothave (item = raw_iron), do #goto 0 0 0; +``` + +#### 语法3:检查位置 + +``` +check me at = (x, y, z), do 动作; +``` + +**参数说明:** +- `(x, y, z)`:目标坐标 +- 容差:±2 格 + +**示例:** +``` +check me at = (0, 85, -3062), do end; // 如果在目标位置,结束宏 +check me at = (0, 0, 0), do #stop; // 如果在目标位置,停止任务 +``` + +#### 语法4:检查时间 + +``` +check time = 时间刻, do 动作; +``` + +**参数说明:** +- `时间刻`:服务器世界时间(0-23999) + - 0 = 6:00 + - 6000 = 12:00 + - 12000 = 18:00 + - 18000 = 0:00(午夜) +- 容差:±50 刻 + +**示例:** +``` +check time = 11000, do /home; // 如果时间到 11000 刻(下午5点),执行 /home +``` + +--- + +### 3. wait 命令 + +阻塞当前线程,等待指定时间或直到宏结束。 + +**语法:** +``` +wait; // 一直阻塞,直到宏结束 +wait xs; // 阻塞 x 秒 +wait xm; // 阻塞 x 分钟 +wait xh; // 阻塞 x 小时 +``` + +**示例:** +``` +wait 5s; // 等待 5 秒 +wait 10m; // 等待 10 分钟 +wait 1h; // 等待 1 小时 +wait; // 一直等待,直到宏被停止 +``` + +--- + +### 4. run 命令 + +立即启动另一个宏(独立执行,不阻塞当前宏)。 + +**语法:** +``` +run name = "宏名称"; +``` + +**示例:** +``` +run name = "回家"; +run name = "挖矿"; +``` + +**注意:** +- 如果指定的宏不存在,会抛出 `NotFanMacroFound` 异常(会被捕获,不会崩溃) +- 启动的宏是独立执行的,不会阻塞当前宏 +- 多个宏可以同时运行,但冲突的命令(如 `goto` 和 `mine`)会导致后启动的宏被停止 + +--- + +### 5. end 命令 + +结束当前执行上下文。 + +**语法:** +``` +do end; +``` + +**说明:** +- 在 `if` 块中使用:只结束 `if` 分支,继续执行主宏流程 +- 在函数中使用:只结束函数执行,继续执行主宏流程 +- 在主宏中使用:结束整个宏 + +**示例:** +``` +if me at = (0, 0, 0) + do end; // 结束 if 块,继续执行主宏 +else + do #goto 0 0 0; +``` + +--- + +## 条件语句 + +### if 语句 + +支持位置检查和时间检查。 + +#### 位置检查 + +**语法:** +``` +if me at = (x, y, z) + // if 块命令 + do #goto 0 0 0; +else + // else 块命令(可选) + do #stop; +end; +``` + +**容差:** ±2 格 + +**示例:** +``` +if me at = (0, 85, -3062) + do #set allowBreak false; + do #set allowPlace false; + do #goto 19 78 -3016; + do #set allowBreak true; + do #set allowPlace true; +else + do end; +``` + +#### 时间检查 + +**语法:** +``` +if time = 时间刻 + // if 块命令 +else + // else 块命令(可选) +end; + +if time >= 时间刻 + // if 块命令 +else + // else 块命令(可选) +end; + +if time <= 时间刻 + // if 块命令 +else + // else 块命令(可选) +end; +``` + +**时间刻说明:** +- 0-23999 刻 = 一天 +- 0 刻 = 6:00 +- 6000 刻 = 12:00 +- 12000 刻 = 18:00 +- 18000 刻 = 0:00(午夜) + +**容差:** ±50 刻 + +**示例:** +``` +fun name="sleep" type= &; + if time = 11000 + do #stop; + do #set allowBreak false; + do #set allowPlace false; + do #goto 0 85 -3062; + do #set allowBreak true; + do #set allowPlace true; + do end; + else + do fun "sleep"; +``` + +### if-else 执行流程 + +**重要:** `if` 块中的命令执行完毕后,**会继续执行主宏的下一个命令**,而不是直接结束整个宏。 + +**示例:** +``` +fan_main: + do fun "goto"; + do fun "sleep"; // if 块执行完毕后,会继续执行这里 + do #mine iron_ore; // 然后执行这里 + +fun name="goto"; + if me at = (0, 85, -3062) + do #set allowBreak false; + do #set allowPlace false; + do #goto 19 78 -3016; // 阻塞等待到达 + do #set allowBreak true; + do #set allowPlace true; + // 执行完毕后,继续执行主宏的下一个命令(do fun "sleep") + else + do end; +``` + +--- + +## 示例代码 + +### 示例1:简单挖矿宏 + +``` +fan_main: + do #goto 19 78 -3016; + do #mine iron_ore; +``` + +### 示例2:带条件检查的挖矿宏 + +``` +fan_main: + do fun "goto"; + do fun "sleep"; + do #mine iron_ore; + +fun name="goto"; + if me at = (0, 85, -3062) + do #set allowBreak false; + do #set allowPlace false; + do #goto 19 78 -3016; + do #set allowBreak true; + do #set allowPlace true; + do fun "sleep"; + do #mine iron_ore; + else + do end; + +fun name="sleep" type= &; + if time = 11000 + do #stop; + do #set allowBreak false; + do #set allowPlace false; + do #goto 0 85 -3062; + do #set allowBreak true; + do #set allowPlace true; + do end; + else + do fun "sleep"; +``` + +### 示例3:使用 check 命令 + +``` +fan_main: + check me nothave (item = diamond), do #mine diamond_ore; + check me have (item = diamond, quantity = 10), do #goto 0 0 0; + check time = 11000, do /home; + do #mine iron_ore; +``` + +### 示例4:使用 run 命令启动其他宏 + +``` +fan_main: + check me nothave (item = diamond), do run name = "挖钻石"; + check me nothave (item = iron_ingot), do run name = "挖铁"; + do #goto 0 0 0; +``` + +--- + +## 注意事项 + +### 1. 命令冲突 + +以下命令不能同时执行(来自不同宏): +- `goto`、`mine`、`explore`、`farm`、`follow` + +如果检测到冲突,后启动的宏会被自动停止。 + +### 2. 阻塞命令 + +以下命令会阻塞宏执行,直到完成: +- `#goto`:等待到达目标位置(容差3格) +- `#mine`:等待挖掘完成 +- `#explore`、`#farm`、`#follow`:等待任务完成 + +### 3. 时间检查 + +- 使用**服务器世界时间**,不是单人存档时间 +- 时间刻范围:0-23999 +- 容差:±50 刻 + +### 4. 位置检查 + +- 容差:±2 格 +- 坐标格式:`(x, y, z)` + +### 5. 物品检查 + +- **非工具物品**:使用精确匹配 + - `diamond` 只匹配 `diamond`,不匹配 `diamond_pickaxe` +- **工具物品**:支持部分匹配 + - `pickaxe` 可以匹配 `diamond_pickaxe` + - 需要指定 `type` 参数来区分材质 + +### 6. 函数执行 + +- **不指定 `type= &`**:函数在主线程执行,会阻塞后续命令 +- **指定 `type= &`**:函数在后台执行,不阻塞主线程 + +### 7. 宏执行流程 + +- 如果宏包含 `if` 条件语句,宏会**循环执行**,持续检查条件 +- 如果宏不包含 `if` 条件语句,宏会**一次性执行**所有命令 + +### 8. 死亡处理 + +- 在服务器模式下,如果玩家死亡,所有正在运行的宏和 Baritone 任务会**立即停止** + +### 9. 错误处理 + +- 如果宏文件格式错误,会在日志中记录错误信息 +- 如果启动不存在的宏,会抛出 `NotFanMacroFound` 异常(会被捕获) + +### 10. 注释 + +- 支持单行注释:`// 这是注释` +- 注释可以放在行尾:`do #goto 0 0 0; // 移动到原点` + +--- + +## 常见问题 + +### Q: 如何停止正在运行的宏? + +A: 通过 Web 界面(端口 8079)或使用 Baritone 的 `#stop` 命令。 + +### Q: 宏文件应该放在哪里? + +A: `.minecraft/config/do/` 目录下,文件名必须是 `.txt` 格式。 + +### Q: 如何查看所有可用的宏? + +A: 通过 Web 界面(端口 8079)访问 `/status` 端点。 + +### Q: 为什么 `check me nothave (item = diamond)` 检查失败? + +A: 确保使用的是精确匹配。如果背包里有 `diamond_pickaxe`,不会匹配 `diamond`。 + +### Q: 如何让函数在后台执行? + +A: 在函数定义中添加 `type= &`:`fun name="函数名" type= &;` + +### Q: `if` 块执行完毕后会怎样? + +A: 会继续执行主宏的下一个命令,而不是结束整个宏。 + +--- + +## 更新日志 + +- **v1.0**:初始版本 + - 支持基本命令(do、if、fun) + - 支持 Baritone 命令和原版命令 + - 支持函数定义和调用 + - 支持条件语句(位置、时间) + - 支持 `check` 命令(物品、位置、时间) + - 支持 `wait` 命令 + - 支持 `run` 命令 + - 支持 `fan_main:` 主入口点 + +--- + +## 技术支持 + +如有问题或建议,请查看日志文件或联系开发者。 + +**日志位置:** `.minecraft/logs/latest.log` + +**Web 界面:** `http://localhost:8079` + diff --git a/autosaveforForge/build.gradle b/autosaveforForge/build.gradle new file mode 100644 index 0000000..0a1957d --- /dev/null +++ b/autosaveforForge/build.gradle @@ -0,0 +1,239 @@ +plugins { + id 'eclipse' + id 'idea' + id 'net.minecraftforge.gradle' version '[6.0.16,6.2)' +} + +group = mod_group_id +version = mod_version + +base { + archivesName = mod_id +} + +java { + toolchain.languageVersion = JavaLanguageVersion.of(17) +} + +minecraft { + // The mappings can be changed at any time and must be in the following format. + // Channel: Version: + // official MCVersion Official field/method names from Mojang mapping files + // parchment YYYY.MM.DD-MCVersion Open community-sourced parameter names and javadocs layered on top of official + // + // You must be aware of the Mojang license when using the 'official' or 'parchment' mappings. + // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md + // + // Parchment is an unofficial project maintained by ParchmentMC, separate from MinecraftForge + // Additional setup is needed to use their mappings: https://parchmentmc.org/docs/getting-started + // + // Use non-default mappings at your own risk. They may not always work. + // Simply re-run your setup task after changing the mappings to update your workspace. + mappings channel: mapping_channel, version: mapping_version + + // When true, this property will have all Eclipse/IntelliJ IDEA run configurations run the "prepareX" task for the given run configuration before launching the game. + // In most cases, it is not necessary to enable. + // enableEclipsePrepareRuns = true + // enableIdeaPrepareRuns = true + + // This property allows configuring Gradle's ProcessResources task(s) to run on IDE output locations before launching the game. + // It is REQUIRED to be set to true for this template to function. + // See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html + copyIdeResources = true + + // When true, this property will add the folder name of all declared run configurations to generated IDE run configurations. + // The folder name can be set on a run configuration using the "folderName" property. + // By default, the folder name of a run configuration is the name of the Gradle project containing it. + // generateRunFolders = true + + // This property enables access transformers for use in development. + // They will be applied to the Minecraft artifact. + // The access transformer file can be anywhere in the project. + // However, it must be at "META-INF/accesstransformer.cfg" in the final mod jar to be loaded by Forge. + // This default location is a best practice to automatically put the file in the right place in the final jar. + // See https://docs.minecraftforge.net/en/latest/advanced/accesstransformers/ for more information. + // accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + // applies to all the run configs below + configureEach { + workingDirectory project.file('run') + + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + property 'forge.logging.markers', 'REGISTRIES' + + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + property 'forge.logging.console.level', 'debug' + + mods { + "${mod_id}" { + source sourceSets.main + } + } + } + + client { + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + property 'forge.enabledGameTestNamespaces', mod_id + } + + server { + property 'forge.enabledGameTestNamespaces', mod_id + args '--nogui' + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + property 'forge.enabledGameTestNamespaces', mod_id + } + + data { + // example of overriding the workingDirectory set in configureEach above + workingDirectory project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') + } + } +} + +// Mixin 配置已移除(不再使用 Mixin 功能) +// mixin { +// add sourceSets.main, "${mod_id}.refmap.json" +// config "${mod_id}.mixins.json" +// } + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + +repositories { + // Put repositories for dependencies here + // ForgeGradle automatically adds the Forge maven and Maven Central for you + + // If you have mod jar dependencies in ./libs, you can declare them as a repository like so. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html#sub:flat_dir_resolver + // flatDir { + // dir 'libs' + // } +} + +dependencies { + // Specify the version of Minecraft to use. + // Any artifact can be supplied so long as it has a "userdev" classifier artifact and is a compatible patcher artifact. + // The "userdev" classifier will be requested and setup by ForgeGradle. + // If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"], + // then special handling is done to allow a setup of a vanilla dependency without the use of an external repository. + minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + + // JSON library for API + implementation 'org.json:json:20231013' + + // Baritone API from bin folder + implementation fg.deobf(files('bin/baritone-api-forge-1.10.3.jar')) + + // Proxy server module - 注意:forgeproxy 是根项目的模块,这里通过文件路径引用 + // 如果 forgeproxy 已编译,可以添加其 jar 文件作为依赖 + // implementation files('../forgeproxy/build/libs/forgeproxy-1.0-SNAPSHOT.jar') + + // Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings + // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime + // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-common-api:${jei_version}") + // compileOnly fg.deobf("mezz.jei:jei-${mc_version}-forge-api:${jei_version}") + // runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}-forge:${jei_version}") + + // Example mod dependency using a mod jar from ./libs with a flat dir repository + // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar + // The group id is ignored when searching -- in this case, it is "blank" + // implementation fg.deobf("blank:coolmod-${mc_version}:${coolmod_version}") + + // For more info: + // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html + // http://www.gradle.org/docs/current/userguide/dependency_management.html + + // Mixin 处理器已移除(不再使用 Mixin 功能) + // annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' + +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.named('processResources', ProcessResources).configure { + var replaceProperties = [ + minecraft_version: minecraft_version, minecraft_version_range: minecraft_version_range, + forge_version: forge_version, forge_version_range: forge_version_range, + loader_version_range: loader_version_range, + mod_id: mod_id, mod_name: mod_name, mod_license: mod_license, mod_version: mod_version, + mod_authors: mod_authors, mod_description: mod_description, + ] + + inputs.properties replaceProperties + + filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { + expand replaceProperties + [project: project] + }} + +// Example for how to get properties into the manifest for reading at runtime. +tasks.named('jar', Jar).configure { + // 修复 Gradle 8.8 增量构建问题 + doNotTrackState("JAR task output may be locked by other processes") + + manifest { + attributes([ + "Specification-Title": mod_id, + "Specification-Vendor": mod_authors, + "Specification-Version": "1", // We are version 1 of ourselves + "Implementation-Title": project.name, + "Implementation-Version": project.version, + "Implementation-Vendor": mod_authors, + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") + ]) + } + + // 注意:不将 Baritone API 打包进 jar 文件 + // Baritone API 应该作为外部依赖,在运行时从 classpath 加载 + // 如果 Baritone mod 已安装,Baritone API 会自动可用 + // 如果必须打包,需要配置 reobfJar 排除 Baritone 类,但推荐使用外部依赖方式 + + // This is the preferred method to reobfuscate your jar file + finalizedBy 'reobfJar' +} + +// 如果需要将 Baritone API 打包进 jar,取消下面的注释并注释掉上面的 jar 任务中的说明 +// 同时需要确保 Baritone mod 在运行时可用,或者将 Baritone API 作为可选依赖 +/* +tasks.named('jar', Jar).configure { + from { + configurations.runtimeClasspath.findAll { + it.name.contains('baritone-api') + }.collect { + it.isDirectory() ? it : zipTree(it) + } + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +// 配置 reobfJar 排除 Baritone 类(避免重新混淆冲突) +afterEvaluate { + tasks.named('reobfJar').configure { + // 排除 Baritone API 的类 + exclude 'baritone/**' + } +} +*/ + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation +} diff --git a/autosaveforForge/gradle.properties b/autosaveforForge/gradle.properties new file mode 100644 index 0000000..5e307ea --- /dev/null +++ b/autosaveforForge/gradle.properties @@ -0,0 +1,55 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false +# 注意:build.bat 脚本会通过环境变量设置 JAVA_HOME +# 这里不需要设置 org.gradle.java.home,因为脚本会处理 + + +# The Minecraft version must agree with the Forge version to get a valid artifact +minecraft_version=1.20.1 +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning conventions. +minecraft_version_range=[1.20.1,1.21) +# The Forge version must agree with the Minecraft version to get a valid artifact +forge_version=47.4.12 +# The Forge version range can use any version of Forge as bounds or match the loader version range +forge_version_range=[47,) +# The loader version range can only use the major version of Forge/FML as bounds +loader_version_range=[47,) +# The mapping channel to use for mappings. +# The default set of supported mapping channels are ["official", "snapshot", "snapshot_nodoc", "stable", "stable_nodoc"]. +# Additional mapping channels can be registered through the "channelProviders" extension in a Gradle plugin. +# +# | Channel | Version | | +# |-----------|----------------------|--------------------------------------------------------------------------------| +# | official | MCVersion | Official field/method names from Mojang mapping files | +# | parchment | YYYY.MM.DD-MCVersion | Open community-sourced parameter names and javadocs layered on top of official | +# +# You must be aware of the Mojang license when using the 'official' or 'parchment' mappings. +# See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md +# +# Parchment is an unofficial project maintained by ParchmentMC, separate from Minecraft Forge. +# Additional setup is needed to use their mappings, see https://parchmentmc.org/docs/getting-started +mapping_channel=official +# The mapping version to query from the mapping channel. +# This must match the format required by the mapping channel. +mapping_version=1.20.1 + + +# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} +# Must match the String constant located in the main mod class annotated with @Mod. +mod_id=autosaveforforge +# The human-readable display name for the mod. +mod_name=autosaveforForge +# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. +mod_license=MIT +# The mod version. See https://semver.org/ +mod_version=1.0-SNAPSHOT +# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. +# This should match the base package used for the mod sources. +# See https://maven.apache.org/guides/mini/guide-naming-conventions.html +mod_group_id=com.xiaofan +# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. +mod_authors=xiaofan, Deepseek +# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. +mod_description=物语农场自动化助手 diff --git a/autosaveforForge/gradlew b/autosaveforForge/gradlew new file mode 100644 index 0000000..f3b75f3 --- /dev/null +++ b/autosaveforForge/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/autosaveforForge/gradlew.bat b/autosaveforForge/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/autosaveforForge/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/autosaveforForge/settings.gradle b/autosaveforForge/settings.gradle new file mode 100644 index 0000000..6155c38 --- /dev/null +++ b/autosaveforForge/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + name = 'MinecraftForge' + url = 'https://maven.minecraftforge.net/' + } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' +} + +rootProject.name = 'autosaveforforge' diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/AutoSleepController.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/AutoSleepController.java new file mode 100644 index 0000000..b16b948 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/AutoSleepController.java @@ -0,0 +1,230 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.HitResult; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.slf4j.Logger; + +/** + * 自动睡觉控制器 + * 当游戏时间到了可以睡觉的时间段时,自动尝试在附近的床上睡觉 + */ +@Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) +public class AutoSleepController { + private static final Logger LOGGER = LogUtils.getLogger(); + + // 可以睡觉的时间范围(游戏时间,0-24000为一个完整的一天) + // 12500 = 晚上7点,23450 = 早上6点 + private static final long SLEEP_START_TIME = 12500L; // 晚上7点 + private static final long SLEEP_END_TIME = 23450L; // 早上6点 + + // 搜索床的范围(以玩家为中心) + private static final int SEARCH_RANGE = 8; + + // 防止频繁尝试睡觉的冷却时间(tick数,20 tick = 1秒) + private static final int COOLDOWN_TICKS = 40; // 2秒 + + private static int cooldownCounter = 0; + private static boolean lastSleepAttemptFailed = false; + + /** + * 监听客户端 Tick 事件 + */ + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + // 只在 tick 结束时检查 + if (event.phase != TickEvent.Phase.END) { + return; + } + + // 冷却时间计数 + if (cooldownCounter > 0) { + cooldownCounter--; + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || mc.player == null) { + return; + } + + LocalPlayer player = mc.player; + + // 检查是否已经在睡觉 + if (player.isSleeping()) { + return; + } + + // 检查是否在游戏中(不在菜单) + if (mc.level == null) { + return; + } + + // 获取游戏时间(dayTime:0-24000,0是早上6点) + long dayTime = mc.level.getDayTime() % 24000; + + // 检查是否在可以睡觉的时间段 + boolean canSleep = false; + if (dayTime >= SLEEP_START_TIME || dayTime < SLEEP_END_TIME) { + canSleep = true; + } + + if (!canSleep) { + // 不在睡觉时间段,重置失败标志 + lastSleepAttemptFailed = false; + return; + } + + // 如果上次尝试失败,增加冷却时间 + if (lastSleepAttemptFailed) { + cooldownCounter = COOLDOWN_TICKS * 2; // 失败后等待更长时间 + lastSleepAttemptFailed = false; + return; + } + + // 尝试找床并睡觉 + BlockPos bedPos = findNearbyBed(player); + if (bedPos != null) { + try { + // 移动到床附近(如果距离太远) + Vec3 playerPos = player.position(); + Vec3 bedVec = Vec3.atCenterOf(bedPos); + double distance = playerPos.distanceTo(bedVec); + + if (distance > 3.0) { + // 床太远,尝试靠近(简单的移动逻辑) + // 这里只是记录日志,实际移动可能需要更复杂的路径查找 + LOGGER.debug("[自动睡觉] 床距离较远 ({} 格),尝试靠近", String.format("%.1f", distance)); + } + + // 尝试在床上睡觉 + if (mc.getConnection() != null && mc.getConnection().getConnection() != null && mc.gameMode != null) { + mc.execute(() -> { + try { + // 获取床的方块状态 + BlockState bedState = mc.level.getBlockState(bedPos); + Block block = bedState.getBlock(); + + // 检查是否是床 + if (block instanceof BedBlock) { + // 检查距离是否足够近(床的交互范围通常是3格) + if (distance <= 3.0) { + LOGGER.info("[自动睡觉] 尝试在床 ({}, {}, {}) 上睡觉,距离: {} 格", + bedPos.getX(), bedPos.getY(), bedPos.getZ(), String.format("%.1f", distance)); + + // 计算床的中心位置(用于交互) + Vec3 bedCenter = Vec3.atCenterOf(bedPos); + + // 创建 BlockHitResult(用于交互) + // 使用床的上表面中心作为点击位置 + Vec3 hitVec = bedCenter.add(0, 0.5, 0); + BlockHitResult hitResult = new BlockHitResult( + hitVec, + Direction.UP, // 从上方点击 + bedPos, + false // 不是内部点击 + ); + + // 使用 GameMode 的 useItemOn 方法与床交互 + // 这会发送数据包到服务器,服务器会处理睡觉逻辑 + // 注意:useItemOn 需要 LocalPlayer 类型 + InteractionResult result = mc.gameMode.useItemOn( + player, + InteractionHand.MAIN_HAND, + hitResult + ); + + if (result.consumesAction()) { + LOGGER.info("[自动睡觉] 成功尝试在床上睡觉"); + lastSleepAttemptFailed = false; + } else { + LOGGER.debug("[自动睡觉] 无法在床上睡觉(可能床被占用或距离太远)"); + lastSleepAttemptFailed = true; + } + } else { + LOGGER.debug("[自动睡觉] 床距离太远 ({} 格),需要靠近", String.format("%.1f", distance)); + lastSleepAttemptFailed = true; + } + } + } catch (Exception e) { + LOGGER.error("[自动睡觉] 尝试睡觉时发生错误", e); + lastSleepAttemptFailed = true; + cooldownCounter = COOLDOWN_TICKS; + } + }); + } + + // 设置冷却时间 + cooldownCounter = COOLDOWN_TICKS; + + } catch (Exception e) { + LOGGER.error("[自动睡觉] 处理睡觉逻辑时发生错误", e); + lastSleepAttemptFailed = true; + cooldownCounter = COOLDOWN_TICKS; + } + } else { + // 没有找到床 + if (cooldownCounter == 0) { + LOGGER.debug("[自动睡觉] 附近没有找到床(搜索范围:{}格)", SEARCH_RANGE); + cooldownCounter = COOLDOWN_TICKS * 3; // 没有床时等待更长时间 + } + } + } + + /** + * 在玩家附近查找床 + * @param player 玩家 + * @return 床的位置,如果没找到返回 null + */ + private static BlockPos findNearbyBed(Player player) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) { + return null; + } + + BlockPos playerPos = player.blockPosition(); + BlockPos closestBed = null; + double closestDistance = Double.MAX_VALUE; + + // 在搜索范围内查找床 + for (int x = -SEARCH_RANGE; x <= SEARCH_RANGE; x++) { + for (int y = -SEARCH_RANGE; y <= SEARCH_RANGE; y++) { + for (int z = -SEARCH_RANGE; z <= SEARCH_RANGE; z++) { + BlockPos checkPos = playerPos.offset(x, y, z); + BlockState state = mc.level.getBlockState(checkPos); + Block block = state.getBlock(); + + // 检查是否是床 + if (block instanceof BedBlock) { + // 检查床是否可用(没有被占用等) + // 注意:在客户端可能无法完全检查占用状态 + + double distance = playerPos.distSqr(checkPos); + if (distance < closestDistance) { + closestDistance = distance; + closestBed = checkPos; + } + } + } + } + } + + return closestBed; + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Autosaveforforge.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Autosaveforforge.java new file mode 100644 index 0000000..a568e46 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Autosaveforforge.java @@ -0,0 +1,163 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.food.FoodProperties; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.BuildCreativeModeTabContentsEvent; +import net.minecraftforge.event.server.ServerStartingEvent; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegistryObject; +import org.slf4j.Logger; + +// The value here should match an entry in the META-INF/mods.toml file +@Mod(Autosaveforforge.MODID) +public class Autosaveforforge { + + // Define mod id in a common place for everything to reference + public static final String MODID = "autosaveforforge"; + // Directly reference a slf4j logger + private static final Logger LOGGER = LogUtils.getLogger(); + // Create a Deferred Register to hold Blocks which will all be registered under the "autosaveforforge" namespace + public static final DeferredRegister BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID); + // Create a Deferred Register to hold Items which will all be registered under the "autosaveforforge" namespace + public static final DeferredRegister ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MODID); + // Create a Deferred Register to hold CreativeModeTabs which will all be registered under the "autosaveforforge" namespace + public static final DeferredRegister CREATIVE_MODE_TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID); + + // Creates a new Block with the id "autosaveforforge:example_block", combining the namespace and path + public static final RegistryObject EXAMPLE_BLOCK = BLOCKS.register("example_block", () -> new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE))); + // Creates a new BlockItem with the id "autosaveforforge:example_block", combining the namespace and path + public static final RegistryObject EXAMPLE_BLOCK_ITEM = ITEMS.register("example_block", () -> new BlockItem(EXAMPLE_BLOCK.get(), new Item.Properties())); + + // Creates a new food item with the id "autosaveforforge:example_id", nutrition 1 and saturation 2 + public static final RegistryObject EXAMPLE_ITEM = ITEMS.register("example_item", () -> new Item(new Item.Properties().food(new FoodProperties.Builder() + .alwaysEat().nutrition(1).saturationMod(2f).build()))); + + // Creates a creative tab with the id "autosaveforforge:example_tab" for the example item, that is placed after the combat tab + public static final RegistryObject EXAMPLE_TAB = CREATIVE_MODE_TABS.register("example_tab", () -> CreativeModeTab.builder() + .withTabsBefore(CreativeModeTabs.COMBAT) + .icon(() -> EXAMPLE_ITEM.get().getDefaultInstance()) + .displayItems((parameters, output) -> { + output.accept(EXAMPLE_ITEM.get()); // Add the example item to the tab. For your own tabs, this method is preferred over the event + }).build()); + + @SuppressWarnings("removal") + public Autosaveforforge() { + IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); + + // Register the commonSetup method for modloading + modEventBus.addListener(this::commonSetup); + + // Register the Deferred Register to the mod event bus so blocks get registered + BLOCKS.register(modEventBus); + // Register the Deferred Register to the mod event bus so items get registered + ITEMS.register(modEventBus); + // Register the Deferred Register to the mod event bus so tabs get registered + CREATIVE_MODE_TABS.register(modEventBus); + + // Register ourselves for server and other game events we are interested in + MinecraftForge.EVENT_BUS.register(this); + + // Register the item to a creative tab + modEventBus.addListener(this::addCreative); + + // Register our mod's ForgeConfigSpec so that Forge can create and load the config file for us + @SuppressWarnings("removal") + var modLoadingContext = net.minecraftforge.fml.ModLoadingContext.get(); + modLoadingContext.registerConfig(net.minecraftforge.fml.config.ModConfig.Type.COMMON, Config.SPEC); + } + + private void commonSetup(final FMLCommonSetupEvent event) { + // Some common setup code + LOGGER.info("HELLO FROM COMMON SETUP"); + LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT)); + + if (Config.logDirtBlock) + LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT)); + + LOGGER.info(Config.magicNumberIntroduction + Config.magicNumber); + + Config.items.forEach((item) -> LOGGER.info("ITEM >> {}", item.toString())); + } + + // Add the example block item to the building blocks tab + private void addCreative(BuildCreativeModeTabContentsEvent event) + { + if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) + event.accept(EXAMPLE_BLOCK_ITEM); + } + // You can use SubscribeEvent and let the Event Bus discover methods to call + @SubscribeEvent + public void onServerStarting(ServerStartingEvent event) { + // Do something when the server starts + LOGGER.info("HELLO from server starting"); + } + + // You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent + @Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) + public static class ClientModEvents { + + @SubscribeEvent + public static void onClientSetup(FMLClientSetupEvent event) + { + // Some client setup code + LOGGER.info("HELLO FROM CLIENT SETUP"); + LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName()); + + // 禁用窗口失去焦点时自动暂停 + event.enqueueWork(() -> { + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.options != null) { + try { + // 在 Minecraft 1.20.1 中,pauseOnLostFocus 可能是一个简单的 boolean 字段 + // 尝试通过反射访问,或者使用其他方法 + java.lang.reflect.Field pauseField = mc.options.getClass().getDeclaredField("pauseOnLostFocus"); + pauseField.setAccessible(true); + pauseField.set(mc.options, false); + LOGGER.info("[性能控制] 已禁用窗口失去焦点时自动暂停(通过反射)"); + } catch (Exception e) { + LOGGER.warn("[性能控制] 无法禁用 pauseOnLostFocus,将使用事件拦截方式: {}", e.getMessage()); + } + } + }); + + // 启动状态 HTTP 服务器 + StatusHttpServer.initialize(); + LOGGER.info("状态 HTTP 服务器已启动,访问 http://localhost:8083 查看游戏状态"); + + // 启动服务器信息 API(端口 2000) + ServerInfoAPI.initialize(); + LOGGER.info("服务器信息 API 已启动,访问 http://localhost:2000 查看 API 文档"); + + // 启动聊天监控 Web 服务器(端口 8081) + ChatMonitorServer.initialize(); + LOGGER.info("聊天监控 Web 服务器已启动,访问 http://localhost:8081 查看聊天监控"); + + // 初始化 Baritone 任务管理器(不自动加载宏) + BaritoneTaskManager.getInstance().initialize(); + LOGGER.info("Baritone 任务管理器已初始化"); + + // 启动宏管理 Web 服务器(端口 8079) + MacroWebServer.initialize(); + LOGGER.info("宏管理 Web 服务器已启动,访问 http://localhost:8079 管理宏"); + } + } +} diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/BaritoneTaskManager.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/BaritoneTaskManager.java new file mode 100644 index 0000000..51d2e3c --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/BaritoneTaskManager.java @@ -0,0 +1,470 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Baritone 自动任务管理器 + * 管理宏文件的加载、解析和执行 + */ +public class BaritoneTaskManager { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String MACRO_FOLDER_NAME = "do"; + private static BaritoneTaskManager instance; + + private final Map macros = new ConcurrentHashMap<>(); + private final Map runningExecutors = new ConcurrentHashMap<>(); + private final Map macroCurrentCommands = new ConcurrentHashMap<>(); // 跟踪每个宏当前执行的命令类型 + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private File macroFolder; + private WatchService watchService; + private Thread watchThread; + private boolean isInitialized = false; + + // 定义冲突命令组(同一组内的命令不能同时执行) + private static final Map> CONFLICT_GROUPS = new HashMap<>(); + static { + // 路径查找组:这些命令会互相冲突 + Set pathfindingGroup = new HashSet<>(); + pathfindingGroup.add("goto"); + pathfindingGroup.add("mine"); + pathfindingGroup.add("explore"); + pathfindingGroup.add("farm"); + pathfindingGroup.add("follow"); + CONFLICT_GROUPS.put("pathfinding", pathfindingGroup); + } + + private BaritoneTaskManager() { + } + + public static BaritoneTaskManager getInstance() { + if (instance == null) { + instance = new BaritoneTaskManager(); + } + return instance; + } + + /** + * 初始化任务管理器(仅初始化文件夹,不加载宏) + */ + public void initialize() { + if (isInitialized) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + LOGGER.warn("[Baritone任务] Minecraft 客户端未初始化,延迟初始化"); + return; + } + + // 获取配置文件夹 + File configDir = new File(mc.gameDirectory, "config"); + if (!configDir.exists()) { + configDir.mkdirs(); + } + + // 创建 do 文件夹 + macroFolder = new File(configDir, MACRO_FOLDER_NAME); + if (!macroFolder.exists()) { + macroFolder.mkdirs(); + LOGGER.info("[Baritone任务] 已创建宏文件夹: {}", macroFolder.getAbsolutePath()); + } + + // 启动文件监听 + startFileWatcher(); + + // 注册事件监听 + MinecraftForge.EVENT_BUS.register(this); + + isInitialized = true; + LOGGER.info("[Baritone任务] 任务管理器已初始化"); + } + + /** + * 获取宏文件夹 + */ + public File getMacroFolder() { + if (!isInitialized) { + initialize(); + } + return macroFolder; + } + + /** + * 手动加载宏 + */ + public void loadMacro(String macroName, Macro macro) { + macros.put(macroName, macro); + LOGGER.info("[Baritone任务] 已加载宏: {}", macroName); + } + + /** + * 加载所有宏文件 + */ + private void loadAllMacros() { + if (macroFolder == null || !macroFolder.exists()) { + return; + } + + File[] files = macroFolder.listFiles((dir, name) -> name.endsWith(".txt")); + if (files == null) { + return; + } + + for (File file : files) { + try { + String macroName = file.getName().replace(".txt", ""); + Macro macro = MacroParser.parse(file); + macros.put(macroName, macro); + LOGGER.info("[Baritone任务] 已加载宏: {}", macroName); + } catch (Exception e) { + LOGGER.error("[Baritone任务] 加载宏文件失败: {}", file.getName(), e); + } + } + } + + /** + * 启动文件监听器 + */ + private void startFileWatcher() { + try { + watchService = FileSystems.getDefault().newWatchService(); + Path path = macroFolder.toPath(); + path.register(watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE); + + watchThread = new Thread(() -> { + try { + while (isInitialized) { + WatchKey key = watchService.take(); + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + if (kind == StandardWatchEventKinds.OVERFLOW) { + continue; + } + + @SuppressWarnings("unchecked") + WatchEvent ev = (WatchEvent) event; + Path fileName = ev.context(); + + if (fileName.toString().endsWith(".txt")) { + String macroName = fileName.toString().replace(".txt", ""); + + if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + macros.remove(macroName); + stopMacro(macroName); + LOGGER.info("[Baritone任务] 已删除宏: {}", macroName); + } else { + // 重新加载宏 + File file = new File(macroFolder, fileName.toString()); + if (file.exists()) { + try { + Macro macro = MacroParser.parse(file); + macros.put(macroName, macro); + LOGGER.info("[Baritone任务] 已重新加载宏: {}", macroName); + } catch (Exception e) { + LOGGER.error("[Baritone任务] 重新加载宏失败: {}", macroName, e); + } + } + } + } + } + key.reset(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + LOGGER.error("[Baritone任务] 文件监听器错误", e); + } + }, "BaritoneMacroWatcher"); + watchThread.setDaemon(true); + watchThread.start(); + + } catch (IOException e) { + LOGGER.error("[Baritone任务] 启动文件监听器失败", e); + } + } + + /** + * 启动宏 + * @param macroName 宏名称 + * @throws NotFanMacroFound 如果宏不存在 + */ + public void startMacro(String macroName) throws NotFanMacroFound { + // 如果宏未加载,先尝试加载所有宏 + if (!macros.containsKey(macroName)) { + loadAllMacros(); + } + + if (!macros.containsKey(macroName)) { + LOGGER.warn("[Baritone任务] 宏不存在: {}", macroName); + throw new NotFanMacroFound(macroName); + } + + // 如果已经在运行,先停止(允许多个宏同时运行,但会检测冲突) + // 注意:不再阻止多个宏同时运行,冲突会在执行命令时检测 + + Macro macro = macros.get(macroName); + MacroExecutor executor = new MacroExecutor(macroName, macro); + runningExecutors.put(macroName, executor); + + executorService.submit(executor); + LOGGER.info("[Baritone任务] 已启动宏: {}", macroName); + } + + /** + * 停止宏 + */ + public void stopMacro(String macroName) { + MacroExecutor executor = runningExecutors.remove(macroName); + if (executor != null) { + executor.stop(); + LOGGER.info("[Baritone任务] 已停止宏: {}", macroName); + } + } + + /** + * 从运行列表中移除宏(由 MacroExecutor 在完成时调用) + * 这个方法用于确保宏执行完成后状态能正确同步 + */ + public void removeRunningMacro(String macroName) { + MacroExecutor executor = runningExecutors.remove(macroName); + macroCurrentCommands.remove(macroName); + if (executor != null) { + LOGGER.info("[Baritone任务] 宏执行完成,已从运行列表移除: {}", macroName); + } + } + + /** + * 检查命令冲突并处理 + * @param macroName 当前执行的宏名 + * @param command 要执行的命令 + * @return true 如果允许执行,false 如果冲突且当前宏被停止 + */ + public boolean checkAndHandleConflict(String macroName, String command) { + try { + // 提取命令类型(去掉 # 和参数) + String commandType = extractCommandType(command); + if (commandType == null) { + return true; // 无法识别的命令,允许执行 + } + + // 检查是否在冲突组中 + Set conflictGroup = null; + for (Set group : CONFLICT_GROUPS.values()) { + if (group.contains(commandType)) { + conflictGroup = group; + break; + } + } + + if (conflictGroup == null) { + // 不在冲突组中,允许执行 + macroCurrentCommands.put(macroName, commandType); + return true; + } + + // 检查是否有其他宏正在执行冲突的命令 + for (Map.Entry entry : macroCurrentCommands.entrySet()) { + String otherMacro = entry.getKey(); + String otherCommand = entry.getValue(); + + // 跳过自己 + if (otherMacro.equals(macroName)) { + continue; + } + + // 检查是否在同一冲突组 + if (conflictGroup.contains(otherCommand)) { + // 发现冲突!停止后执行的宏(当前宏) + LOGGER.warn("[Baritone任务] 检测到命令冲突: 宏 {} 正在执行 {},与宏 {} 的 {} 冲突", + otherMacro, otherCommand, macroName, commandType); + + logToChatAndLogger(String.format("[宏冲突] 宏 %s 正在执行 %s,与宏 %s 的 %s 冲突,停止宏 %s", + otherMacro, otherCommand, macroName, commandType, macroName)); + + // 停止当前宏(后执行的) + stopMacro(macroName); + return false; + } + } + + // 没有冲突,记录当前命令 + macroCurrentCommands.put(macroName, commandType); + return true; + + } catch (Exception e) { + LOGGER.error("[Baritone任务] 检查命令冲突时出错", e); + // 出错时允许执行,避免阻塞 + return true; + } + } + + /** + * 清除宏的命令记录(命令执行完成时调用) + */ + public void clearMacroCommand(String macroName) { + macroCurrentCommands.remove(macroName); + } + + /** + * 提取命令类型(去掉 # 和参数) + */ + private String extractCommandType(String command) { + if (command == null || command.isEmpty()) { + return null; + } + + String trimmed = command.trim(); + if (trimmed.startsWith("#")) { + trimmed = trimmed.substring(1); + } + + // 提取第一个单词(命令名) + int spaceIndex = trimmed.indexOf(' '); + if (spaceIndex > 0) { + return trimmed.substring(0, spaceIndex).toLowerCase(); + } + return trimmed.toLowerCase(); + } + + /** + * 记录到日志和聊天框 + */ + private void logToChatAndLogger(String message) { + LOGGER.info(message); + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.player != null) { + mc.execute(() -> { + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§7[宏冲突] §r" + message)); + } + }); + } + } + + /** + * 停止所有宏 + */ + public void stopAllMacros() { + for (String macroName : new ArrayList<>(runningExecutors.keySet())) { + stopMacro(macroName); + } + } + + /** + * 获取所有宏名称 + */ + public Set getMacroNames() { + return new HashSet<>(macros.keySet()); + } + + /** + * 检查宏是否在运行 + */ + public boolean isMacroRunning(String macroName) { + return runningExecutors.containsKey(macroName); + } + + /** + * 游戏刻事件,用于检查时间条件 + */ + @Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) + public static class GameTickHandler { + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + + // 更新所有运行中的执行器 + BaritoneTaskManager manager = getInstance(); + for (MacroExecutor executor : new ArrayList<>(manager.runningExecutors.values())) { + executor.onTick(); + } + } + } + + /** + * 关闭任务管理器 + */ + public void shutdown() { + isInitialized = false; + stopAllMacros(); + + if (watchService != null) { + try { + watchService.close(); + } catch (IOException e) { + LOGGER.error("[Baritone任务] 关闭文件监听器失败", e); + } + } + + if (watchThread != null && watchThread.isAlive()) { + watchThread.interrupt(); + } + + executorService.shutdown(); + LOGGER.info("[Baritone任务] 任务管理器已关闭"); + } + + /** + * 获取当前游戏时间(刻) + * 返回服务器世界时间,不是客户端本地时间 + */ + public static long getCurrentTime() { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) { + return 0; + } + + // getDayTime() 返回的是服务器同步的世界时间 + // 在多人游戏中,这是服务器的时间,不是客户端本地时间 + long worldTime = mc.level.getDayTime(); + + // 返回一天内的时间(0-23999刻) + long dayTime = worldTime % 24000; + + LOGGER.debug("[时间检查] 世界时间={}, 一天内时间={}", worldTime, dayTime); + + return dayTime; + } + + /** + * 获取玩家当前位置 + */ + public static BlockPos getPlayerPosition() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + return null; + } + return mc.player.blockPosition(); + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ChatMonitorServer.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ChatMonitorServer.java new file mode 100644 index 0000000..ba2acae --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ChatMonitorServer.java @@ -0,0 +1,740 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; + +/** + * Forge 版本的聊天监控 Web 服务器 + * 端口: 8081 + * 功能: 实时监控游戏聊天消息,提供 Web 界面和 API + */ +public class ChatMonitorServer { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int HTTP_PORT = 8081; + private static HttpServer httpServer; + private static final ConcurrentHashMap chatHistory = new ConcurrentHashMap<>(); + private static boolean isInGame = false; + private static boolean isEnabled = true; + + /** + * 初始化聊天监控服务器 + */ + public static void initialize() { + // 注册消息监听器 + MinecraftForge.EVENT_BUS.register(new ChatMessageListener()); + + // 启动 HTTP 服务器 + if (isEnabled) { + startHttpServer(); + } + } + + public static void setEnabled(boolean enabled) { + isEnabled = enabled; + if (!enabled) { + stopHttpServer(); + } else { + startHttpServer(); + } + } + + public static boolean isEnabled() { + return isEnabled; + } + + private static void startHttpServer() { + if (httpServer != null) { + return; + } + + try { + // 先尝试停止可能存在的旧服务器 + stop(); + + httpServer = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + httpServer.createContext("/", new WebInterfaceHandler()); + httpServer.createContext("/chat", new ChatHandler()); + httpServer.createContext("/status", new StatusHandler()); + httpServer.createContext("/clear", new ClearHandler()); + httpServer.createContext("/latest", new LatestHandler()); + httpServer.createContext("/send", new SendHandler()); + httpServer.setExecutor(Executors.newCachedThreadPool()); + httpServer.start(); + + addSystemMessage("§a[聊天监控] HTTP服务器已启动 (端口: " + HTTP_PORT + ")"); + addSystemMessage("§a[聊天监控] 访问 http://localhost:" + HTTP_PORT + " 查看聊天监控"); + LOGGER.info("[聊天监控] HTTP服务器已启动 (端口: {})", HTTP_PORT); + + } catch (java.net.BindException e) { + LOGGER.error("[聊天监控] 端口 {} 已被占用", HTTP_PORT); + isEnabled = false; + } catch (IOException e) { + LOGGER.error("[聊天监控] 启动失败: {}", e.getMessage(), e); + addSystemMessage("§c[聊天监控] 启动失败: " + e.getMessage()); + isEnabled = false; + } + } + + private static void stopHttpServer() { + if (httpServer != null && isEnabled) { + try { + httpServer.stop(0); + httpServer = null; + addSystemMessage("§e[聊天监控] HTTP服务器已停止"); + LOGGER.info("[聊天监控] HTTP服务器已停止"); + } catch (Exception e) { + LOGGER.error("[聊天监控] 停止失败: {}", e.getMessage()); + } + } + } + + public static void stop() { + stopHttpServer(); + } + + /** + * 监听游戏进入/退出事件 + */ + @Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) + public static class GameStateListener { + @SubscribeEvent + public static void onClientTick(net.minecraftforge.event.TickEvent.ClientTickEvent event) { + if (event.phase != net.minecraftforge.event.TickEvent.Phase.END) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + boolean currentlyInGame = mc != null && mc.level != null && mc.player != null; + + // 检测游戏状态变化 + if (currentlyInGame != isInGame) { + isInGame = currentlyInGame; + if (isInGame) { + if (isEnabled && httpServer == null) { + startHttpServer(); + } + addSystemMessage("已进入游戏世界"); + } else { + stopHttpServer(); + addSystemMessage("已退出游戏世界"); + } + } + } + } + + /** + * 聊天消息监听器 + */ + @Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) + public static class ChatMessageListener { + @SubscribeEvent + public static void onChatReceived(ClientChatReceivedEvent event) { + if (!isEnabled) { + return; + } + + Component messageComponent = event.getMessage(); + if (messageComponent == null) { + return; + } + + String messageText = messageComponent.getString(); + if (messageText != null && !messageText.trim().isEmpty()) { + addChatMessage(messageText); + } + } + } + + public static void addChatMessage(String message) { + if (!isEnabled || message.trim().isEmpty()) return; + + String timestamp = String.valueOf(System.currentTimeMillis()); + chatHistory.put(timestamp, message); + + // 限制最多 1000 条消息 + if (chatHistory.size() > 1000) { + String oldestKey = chatHistory.keys().nextElement(); + chatHistory.remove(oldestKey); + } + } + + public static void clearChatHistory() { + chatHistory.clear(); + addSystemMessage("§a[聊天监控] 聊天记录已清空"); + } + + /** + * 发送消息到游戏 + */ + public static boolean sendToGame(String message) { + if (!isInGame || message.trim().isEmpty()) { + return false; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null && mc.getConnection() != null) { + String trimmedMessage = message.trim(); + + // 在主游戏线程中执行 + mc.execute(() -> { + try { + // 如果是命令(以/开头),发送命令 + if (trimmedMessage.startsWith("/")) { + mc.getConnection().sendCommand(trimmedMessage.substring(1)); + } else { + // 普通聊天消息(Forge 直接发送字符串) + mc.getConnection().sendChat(trimmedMessage); + } + + // 记录发送的消息 + addChatMessage("【网页发送】" + trimmedMessage); + } catch (Exception e) { + LOGGER.error("[聊天监控] 发送消息失败: {}", e.getMessage(), e); + } + }); + + return true; + } + return false; + } + + private static void addSystemMessage(String message) { + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.player != null) { + mc.execute(() -> { + mc.player.sendSystemMessage(Component.literal(message)); + }); + } + addChatMessage(message); + } + + private static String getGameStatus() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + return "menu"; + } + + if (mc.getCurrentServer() != null) { + return "multiplayer - " + mc.getCurrentServer().ip; + } else if (mc.isSingleplayer() && mc.level != null) { + return "singleplayer"; + } else { + return "menu"; + } + } + + /** + * Web 界面处理器 + */ + private static class WebInterfaceHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + String html = getWebInterfaceHTML(); + byte[] bytes = html.getBytes(StandardCharsets.UTF_8); + + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + + private String getWebInterfaceHTML() { + return """ + + + + + + Minecraft 聊天监控 + + + +
+
+

🎮 Minecraft 聊天监控 (Forge)

+

实时显示游戏聊天消息 | 支持网页发送消息和命令

+
+ +
+
+
+ 连接状态: 检查中... +
+
+ 消息数量: 0 +
+
+ 游戏状态: 未知 +
+
+ +
+
+ + +
+
+ 提示:以 / 开头的消息将作为命令执行,例如 /time set day +
+
+ +
+
+
系统消息
+
聊天监控已启动,等待消息...
+
+
+ +
+ + +
+ +
+

端口: 8081 | 最后更新: -

+
+
+ + + + + """; + } + } + + /** + * 获取所有聊天消息处理器 + */ + private static class ChatHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + StringBuilder json = new StringBuilder("{\"messages\":["); + chatHistory.forEach((time, msg) -> { + String escaped = escapeJson(msg); + json.append(String.format("{\"timestamp\":%s,\"content\":\"%s\"},", time, escaped)); + }); + + if (json.charAt(json.length()-1) == ',') { + json.deleteCharAt(json.length()-1); + } + + json.append("],\"count\":").append(chatHistory.size()) + .append(",\"game_status\":\"").append(getGameStatus()) + .append("\"}"); + + sendResponse(exchange, json.toString()); + } + } + + /** + * 状态处理器 + */ + private static class StatusHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + String response = String.format( + "{\"enabled\":%s,\"in_game\":%s,\"game_status\":\"%s\",\"port\":%d,\"message_count\":%d,\"server_running\":%s}", + isEnabled, isInGame, getGameStatus(), HTTP_PORT, chatHistory.size(), (httpServer != null) + ); + + sendResponse(exchange, response); + } + } + + /** + * 清空聊天记录处理器 + */ + private static class ClearHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + clearChatHistory(); + sendResponse(exchange, "{\"status\":\"success\",\"message\":\"Chat history cleared\"}"); + } + } + + /** + * 获取最新消息处理器 + */ + private static class LatestHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + String query = exchange.getRequestURI().getQuery(); + int limit = 50; + + if (query != null && query.startsWith("limit=")) { + try { + limit = Integer.parseInt(query.substring("limit=".length())); + limit = Math.min(Math.max(limit, 1), 100); + } catch (NumberFormatException e) { + // 使用默认值 + } + } + + StringBuilder json = new StringBuilder("{\"latest_messages\":["); + + chatHistory.entrySet().stream() + .sorted((e1, e2) -> Long.compare(Long.parseLong(e2.getKey()), Long.parseLong(e1.getKey()))) + .limit(limit) + .forEach(entry -> { + String escaped = escapeJson(entry.getValue()); + json.append(String.format("{\"timestamp\":%s,\"content\":\"%s\"},", entry.getKey(), escaped)); + }); + + if (json.charAt(json.length()-1) == ',') { + json.deleteCharAt(json.length()-1); + } + + json.append("],\"count\":").append(chatHistory.size()).append("}"); + + sendResponse(exchange, json.toString()); + } + } + + /** + * 发送消息处理器 + */ + private static class SendHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + // 获取查询参数 + String query = exchange.getRequestURI().getQuery(); + if (query == null || !query.startsWith("message=")) { + sendError(exchange, 400, "Missing message parameter"); + return; + } + + String message = query.substring("message=".length()); + message = java.net.URLDecoder.decode(message, StandardCharsets.UTF_8); + + boolean success = sendToGame(message); + String response; + + if (success) { + response = "{\"status\":\"success\",\"message\":\"Message sent to game\"}"; + } else { + response = "{\"status\":\"error\",\"message\":\"Failed to send message - not in game\"}"; + } + + sendResponse(exchange, response); + } + } + + /** + * 工具方法 + */ + private static String escapeJson(String input) { + if (input == null) { + return ""; + } + return input.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + private static void sendResponse(HttpExchange exchange, String response) throws IOException { + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); + exchange.sendResponseHeaders(200, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + + private static void sendError(HttpExchange exchange, int code, String message) throws IOException { + String response = "{\"error\":\"" + escapeJson(message) + "\",\"code\":" + code + "}"; + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(code, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Config.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Config.java new file mode 100644 index 0000000..bb05c82 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/Config.java @@ -0,0 +1,82 @@ +package com.xiaofan.autosaveforforge; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.config.ModConfigEvent; +import net.minecraftforge.registries.ForgeRegistries; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +// An example config class. This is not required, but it's a good idea to have one to keep your config organized. +// Demonstrates how to use Forge's config APIs +@Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) +public class Config +{ + private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder(); + + private static final ForgeConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER + .comment("Whether to log the dirt block on common setup") + .define("logDirtBlock", true); + + private static final ForgeConfigSpec.IntValue MAGIC_NUMBER = BUILDER + .comment("A magic number") + .defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE); + + public static final ForgeConfigSpec.ConfigValue MAGIC_NUMBER_INTRODUCTION = BUILDER + .comment("What you want the introduction message to be for the magic number") + .define("magicNumberIntroduction", "The magic number is... "); + + // a list of strings that are treated as resource locations for items + private static final ForgeConfigSpec.ConfigValue> ITEM_STRINGS = BUILDER + .comment("A list of items to log on common setup.") + .defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), Config::validateItemName); + + static final ForgeConfigSpec SPEC = BUILDER.build(); + + public static boolean logDirtBlock; + public static int magicNumber; + public static String magicNumberIntroduction; + public static Set items; + + @SuppressWarnings("removal") + private static boolean validateItemName(final Object obj) + { + if (!(obj instanceof final String itemName)) { + return false; + } + try { + ResourceLocation location = new ResourceLocation(itemName); + return ForgeRegistries.ITEMS.containsKey(location); + } catch (Exception e) { + return false; + } + } + + @SubscribeEvent + static void onLoad(final ModConfigEvent event) + { + logDirtBlock = LOG_DIRT_BLOCK.get(); + magicNumber = MAGIC_NUMBER.get(); + magicNumberIntroduction = MAGIC_NUMBER_INTRODUCTION.get(); + + // convert the list of strings into a set of items + items = ITEM_STRINGS.get().stream() + .map(itemName -> { + try { + @SuppressWarnings("removal") + ResourceLocation location = new ResourceLocation(itemName); + return ForgeRegistries.ITEMS.getValue(location); + } catch (Exception e) { + return null; + } + }) + .filter(item -> item != null) + .collect(Collectors.toSet()); + } +} diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DeathHandler.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DeathHandler.java new file mode 100644 index 0000000..67193db --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DeathHandler.java @@ -0,0 +1,109 @@ +package com.xiaofan.autosaveforforge; + +import baritone.api.BaritoneAPI; +import baritone.api.IBaritone; +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.slf4j.Logger; + +/** + * 死亡处理 + * 通过 Tick 事件检测玩家死亡,死亡时立即停止所有 Baritone 任务和宏 + * 适用于服务器设置了立即重生的场景(没有死亡菜单) + */ +@Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) +public class DeathHandler { + private static final Logger LOGGER = LogUtils.getLogger(); + private static boolean wasDead = false; // 上次检查时的死亡状态 + + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) { + return; + } + + try { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || mc.level == null) { + wasDead = false; + return; + } + + // 确保在服务器模式下(不是单人存档) + // 在单人存档中,mc.getCurrentServer() 返回 null + // 在服务器模式下,mc.getCurrentServer() 不为 null 或 mc.getConnection() 不为 null + if (mc.isSingleplayer()) { + // 单人存档,跳过检测 + wasDead = false; + return; + } + + // 检查是否连接到服务器 + if (mc.getConnection() == null) { + wasDead = false; + return; + } + + // 检查玩家是否死亡(服务器模式下的死亡状态) + boolean isDead = mc.player.isDeadOrDying(); + + // 检测从存活到死亡的状态变化 + if (!wasDead && isDead) { + LOGGER.info("[死亡处理] 检测到玩家死亡(服务器模式),正在停止所有 Baritone 任务和宏"); + + // 在主游戏线程中执行 + mc.execute(() -> { + try { + stopAllBaritoneTasks(mc); + } catch (Exception e) { + LOGGER.error("[死亡处理] 停止 Baritone 任务时出错", e); + } + }); + } + + // 更新状态 + wasDead = isDead; + + } catch (Exception e) { + LOGGER.error("[死亡处理] 检测死亡状态时出错", e); + } + } + + /** + * 停止所有 Baritone 任务和宏 + */ + private static void stopAllBaritoneTasks(Minecraft mc) { + try { + LOGGER.info("[死亡处理] 正在停止所有 Baritone 任务和宏..."); + + // 1. 停止所有正在运行的宏 + BaritoneTaskManager.getInstance().stopAllMacros(); + LOGGER.info("[死亡处理] 已停止所有宏"); + + // 2. 执行 Baritone 的 stop 命令,停止所有 Baritone 任务 + try { + IBaritone baritone = BaritoneAPI.getProvider().getPrimaryBaritone(); + if (baritone != null) { + baritone.getCommandManager().execute("stop"); + LOGGER.info("[死亡处理] 已执行 Baritone stop 命令"); + if (mc.player != null) { + mc.player.sendSystemMessage(Component.literal("§7[死亡处理] 已停止所有 Baritone 任务和宏")); + } + } else { + LOGGER.debug("[死亡处理] Baritone 未加载,跳过 stop 命令"); + } + } catch (Exception e) { + LOGGER.warn("[死亡处理] 执行 Baritone stop 命令失败: {}", e.getMessage()); + } + + } catch (Exception e) { + LOGGER.error("[死亡处理] 停止 Baritone 任务时出错", e); + } + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DoCommandHandler.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DoCommandHandler.java new file mode 100644 index 0000000..5f8bbd7 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/DoCommandHandler.java @@ -0,0 +1,179 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.ClientChatEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.slf4j.Logger; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * /do 客户端命令处理器 + * 通过拦截客户端发送的聊天消息来处理 /do list 和 /do <宏文件名> 命令 + * 这是纯客户端命令,不会发送到服务器 + */ +public class DoCommandHandler { + private static final Logger LOGGER = LogUtils.getLogger(); + private static boolean initialized = false; + + /** + * 初始化客户端命令处理器并注册事件监听 + */ + public static void initialize() { + if (initialized) { + return; + } + // 手动注册事件监听器 + MinecraftForge.EVENT_BUS.register(DoCommandHandler.class); + initialized = true; + LOGGER.info("[Do命令] 客户端命令处理器已初始化并注册事件监听"); + } + + /** + * 拦截客户端发送的聊天消息,处理 /do 命令 + * 这是客户端命令,不会发送到服务器 + */ + @SubscribeEvent + public static void onClientChat(ClientChatEvent event) { + String message = event.getMessage(); + if (message == null || !message.trim().startsWith("/do")) { + return; + } + + LOGGER.info("[Do命令] 检测到 /do 命令: {}", message); + + // 取消发送到服务器(这是客户端命令) + event.setCanceled(true); + + // 处理命令 + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + LOGGER.warn("[Do命令] 玩家未初始化,无法执行命令"); + return; + } + + String command = message.trim(); + String[] parts = command.split("\\s+", 3); + + if (parts.length == 1 || (parts.length == 2 && parts[1].equals("list"))) { + // /do 或 /do list + LOGGER.info("[Do命令] 执行 list 命令"); + listMacros(mc); + } else if (parts.length >= 2) { + // /do <宏文件名> + String macroName = parts[1]; + LOGGER.info("[Do命令] 执行宏: {}", macroName); + executeMacro(mc, macroName); + } + } + + /** + * 列出所有宏文件 + */ + private static void listMacros(Minecraft mc) { + try { + BaritoneTaskManager manager = BaritoneTaskManager.getInstance(); + if (manager.getMacroFolder() == null) { + manager.initialize(); + } + + File macroFolder = manager.getMacroFolder(); + if (macroFolder == null || !macroFolder.exists()) { + sendMessage(mc, "§c宏文件夹不存在"); + return; + } + + File[] files = macroFolder.listFiles((dir, name) -> name.endsWith(".txt")); + if (files == null || files.length == 0) { + sendMessage(mc, "§e没有找到宏文件"); + return; + } + + List macroNames = new ArrayList<>(); + for (File file : files) { + macroNames.add(file.getName()); + } + + // 发送消息 + sendMessage(mc, "§a有 " + macroNames.size() + " 个宏文件:"); + + for (int i = 0; i < macroNames.size(); i++) { + sendMessage(mc, "§7" + (i + 1) + "." + macroNames.get(i)); + } + + } catch (Exception e) { + LOGGER.error("[Do命令] 列出宏文件时出错", e); + sendMessage(mc, "§c列出宏文件时出错: " + e.getMessage()); + } + } + + /** + * 执行宏 + */ + private static void executeMacro(Minecraft mc, String macroName) { + try { + // 如果用户输入了 .txt,去掉它 + if (macroName.endsWith(".txt")) { + macroName = macroName.substring(0, macroName.length() - 4); + } + + // 加载并执行宏 + BaritoneTaskManager manager = BaritoneTaskManager.getInstance(); + + // 确保宏文件夹已初始化 + if (manager.getMacroFolder() == null) { + manager.initialize(); + } + + // 加载宏文件 + File macroFile = new File(manager.getMacroFolder(), macroName + ".txt"); + if (!macroFile.exists()) { + sendMessage(mc, "§c宏文件不存在: " + macroName + ".txt"); + return; + } + + // 如果宏已经在运行,先停止 + if (manager.isMacroRunning(macroName)) { + manager.stopMacro(macroName); + sendMessage(mc, "§e已停止正在运行的宏: " + macroName); + } + + // 加载宏 + try { + Macro macro = MacroParser.parse(macroFile); + manager.loadMacro(macroName, macro); + + // 启动宏 + manager.startMacro(macroName); + sendMessage(mc, "§a已启动宏: " + macroName); + } catch (NotFanMacroFound e) { + LOGGER.error("[Do命令] 宏不存在: " + macroName); + sendMessage(mc, "§c宏不存在: " + macroName); + } catch (Exception e) { + LOGGER.error("[Do命令] 加载宏文件失败: " + macroName, e); + sendMessage(mc, "§c加载宏文件失败: " + e.getMessage()); + } + + } catch (Exception e) { + LOGGER.error("[Do命令] 执行宏时出错", e); + sendMessage(mc, "§c执行宏时出错: " + e.getMessage()); + } + } + + /** + * 发送消息到聊天 + */ + private static void sendMessage(Minecraft mc, String message) { + if (mc.player != null) { + mc.player.sendSystemMessage(Component.literal(message)); + } + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroExecutor.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroExecutor.java new file mode 100644 index 0000000..b1d498b --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroExecutor.java @@ -0,0 +1,1109 @@ +package com.xiaofan.autosaveforforge; + +import baritone.api.BaritoneAPI; +import baritone.api.IBaritone; +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * 宏执行器 + * 执行宏命令 + */ +public class MacroExecutor implements Runnable { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final String macroName; + private final Macro macro; + private final AtomicBoolean stopped = new AtomicBoolean(false); + private int currentCommandIndex = 0; + + public MacroExecutor(String macroName, Macro macro) { + this.macroName = macroName; + this.macro = macro; + } + + @Override + public void run() { + logToChatAndLogger("[宏执行] 开始执行宏: " + macroName); + LOGGER.info("[宏执行] 开始执行宏: {}", macroName); + + try { + executeMacro(macro); + } catch (Exception e) { + logToChatAndLogger("[宏执行] 执行宏时出错: " + macroName + " - " + e.getMessage()); + LOGGER.error("[宏执行] 执行宏时出错: {}", macroName, e); + } finally { + logToChatAndLogger("[宏执行] 宏执行结束: " + macroName); + LOGGER.info("[宏执行] 宏执行结束: {}", macroName); + + // 从运行列表中移除,确保状态同步 + BaritoneTaskManager.getInstance().removeRunningMacro(macroName); + } + } + + /** + * 同时记录到日志和聊天框 + */ + private void logToChatAndLogger(String message) { + LOGGER.info(message); + + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.player != null) { + // 在主游戏线程中显示消息 + mc.execute(() -> { + if (mc.player != null) { + // 使用系统消息,不会发送到服务器 + mc.player.sendSystemMessage(Component.literal("§7[宏] §r" + message)); + } + }); + } + } + + /** + * 执行宏 + * 如果宏包含 if 条件,需要循环执行以持续检查条件 + */ + private void executeMacro(Macro macro) { + String macroInfo = String.format("[宏执行] 宏包含 %d 个命令", macro.commands.size()); + logToChatAndLogger(macroInfo); + LOGGER.info("[宏执行] 宏包含 {} 个命令", macro.commands.size()); + if (macro.commands.isEmpty()) { + logToChatAndLogger("[宏执行] 宏文件没有解析出任何命令,请检查宏文件格式"); + LOGGER.warn("[宏执行] 宏文件没有解析出任何命令,请检查宏文件格式"); + return; + } + + // 检查是否包含 if 条件(需要循环执行) + boolean hasConditional = macro.commands.stream() + .anyMatch(cmd -> cmd instanceof IfStatement); + + if (hasConditional) { + // 包含条件语句,需要循环执行 + logToChatAndLogger("[宏执行] 宏包含条件语句,将循环执行"); + LOGGER.info("[宏执行] 宏包含条件语句,将循环执行"); + while (!stopped.get()) { + for (int i = 0; i < macro.commands.size(); i++) { + if (stopped.get()) { + LOGGER.info("[宏执行] 宏已被停止,停止执行剩余命令"); + return; + } + MacroCommand cmd = macro.commands.get(i); + String cmdInfo = String.format("[宏执行] 执行第 %d 个命令: %s", i + 1, cmd.getClass().getSimpleName()); + logToChatAndLogger(cmdInfo); + LOGGER.info("[宏执行] 执行第 {} 个命令: {}", i + 1, cmd.getClass().getSimpleName()); + try { + cmd.execute(this); + // 如果是条件语句,等待一小段时间再检查 + if (cmd instanceof IfStatement) { + try { + Thread.sleep(100); // 等待100ms再检查条件 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } catch (Exception e) { + LOGGER.error("[宏执行] 执行命令时出错", e); + } + } + // 每次循环之间等待一段时间 + try { + Thread.sleep(500); // 等待500ms再进行下一轮检查 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } else { + // 不包含条件语句,一次性执行完 + for (int i = 0; i < macro.commands.size(); i++) { + if (stopped.get()) { + LOGGER.info("[宏执行] 宏已被停止,停止执行剩余命令"); + break; + } + MacroCommand cmd = macro.commands.get(i); + LOGGER.info("[宏执行] 执行第 {} 个命令: {}", i + 1, cmd.getClass().getSimpleName()); + try { + cmd.execute(this); + } catch (Exception e) { + LOGGER.error("[宏执行] 执行命令时出错", e); + } + } + } + } + + /** + * 执行 do 命令 + */ + public void executeDoCommand(String content) { + if (stopped.get()) { + return; + } + + // 检查是否是函数调用 do fun "name"; + if (content.startsWith("fun ")) { + String funcName = content.substring(4).trim(); + if (funcName.startsWith("\"") && funcName.endsWith("\"")) { + funcName = funcName.substring(1, funcName.length() - 1); + } else if (funcName.endsWith("\"")) { + // 处理 do fun "name"; 格式 + int quoteStart = funcName.indexOf("\""); + if (quoteStart >= 0) { + funcName = funcName.substring(quoteStart + 1, funcName.length() - 1); + } + } + callFunction(funcName); + return; + } + + // 检查是否是 end 命令 + // 注意:end 命令只停止当前执行上下文,不会阻止后续命令的执行 + // 只有当在 if/else 分支中执行 end 时,才会停止该分支的执行 + if (content.equals("end")) { + logToChatAndLogger("[宏执行] 执行 end 命令,停止当前执行上下文"); + LOGGER.info("[宏执行] 执行 end 命令,停止当前执行上下文"); + stop(); + return; + } + + // 检查是否是 wait 命令 + if (content.startsWith("wait")) { + String waitContent = content.substring(4).trim(); + executeWaitCommand(waitContent); + return; + } + + // 检查是否是原版命令(/开头) + if (content.startsWith("/")) { + String command = content.substring(1).trim(); // 去掉 / 前缀 + executeMinecraftCommand(command); + return; + } + + // 检查是否是 baritone 命令(#开头) + if (content.startsWith("#")) { + String baritoneCmd = content.substring(1).trim(); + // 检查是否是阻塞命令(需要等待执行完成) + boolean isBlocking = isBlockingCommand(baritoneCmd); + if (isBlocking) { + logToChatAndLogger("[宏执行] 检测到阻塞命令: " + baritoneCmd + ",将等待执行完成"); + LOGGER.info("[宏执行] 检测到阻塞命令: {},将等待执行完成", baritoneCmd); + executeBaritoneCommandBlocking(baritoneCmd); + } else { + executeBaritoneCommand(baritoneCmd); + } + } else { + LOGGER.warn("[宏执行] 未知命令: {}", content); + } + } + + /** + * 执行原版 Minecraft 命令 + * @param command 命令内容(不包含 / 前缀) + */ + private void executeMinecraftCommand(String command) { + if (stopped.get()) { + return; + } + + logToChatAndLogger("[宏执行] 准备执行原版命令: /" + command); + LOGGER.info("[宏执行] 准备执行原版命令: /{}", command); + + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) { + LOGGER.warn("[宏执行] 无法执行原版命令,玩家未初始化"); + return; + } + + if (mc.getConnection() == null) { + LOGGER.warn("[宏执行] 无法执行原版命令,未连接到服务器"); + return; + } + + // 确保在主游戏线程中执行 + if (mc.isSameThread()) { + // 已经在主线程,直接执行 + executeMinecraftCommandInternal(command); + } else { + // 不在主线程,切换到主线程执行 + final String cmd = command; + mc.execute(() -> executeMinecraftCommandInternal(cmd)); + } + } + + /** + * 内部方法:实际执行原版命令 + */ + private void executeMinecraftCommandInternal(String command) { + try { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getConnection() == null || mc.player == null) { + LOGGER.warn("[宏执行] 无法执行原版命令,连接或玩家未初始化"); + return; + } + + // 发送命令到服务器(sendCommand 不需要 / 前缀) + mc.getConnection().sendCommand(command); + + logToChatAndLogger("[宏执行] ✓ 已发送原版命令: /" + command); + LOGGER.info("[宏执行] ✓ 已发送原版命令: /{}", command); + + } catch (Exception e) { + LOGGER.error("[宏执行] 执行原版命令时出错: /{}", command, e); + logToChatAndLogger("[宏执行] 执行原版命令时出错: /" + command + " - " + e.getMessage()); + } + } + + /** + * 调用函数 + * 函数内部可以访问宏的所有函数定义,支持递归调用 + */ + private void callFunction(String funcName) { + logToChatAndLogger("[宏执行] 尝试调用函数: " + funcName); + logToChatAndLogger("[宏执行] 当前宏包含的函数: " + macro.functions.keySet()); + LOGGER.info("[宏执行] 尝试调用函数: {}", funcName); + LOGGER.info("[宏执行] 当前宏包含的函数: {}", macro.functions.keySet()); + + Function func = macro.functions.get(funcName); + if (func == null) { + logToChatAndLogger("[宏执行] 函数不存在: " + funcName + ",可用函数: " + macro.functions.keySet()); + LOGGER.warn("[宏执行] 函数不存在: {},可用函数: {}", funcName, macro.functions.keySet()); + return; + } + + String funcInfo = String.format("[宏执行] 调用函数: %s (后台执行: %s),函数包含 %d 个命令", + funcName, func.isBackground, func.commands.size()); + logToChatAndLogger(funcInfo); + LOGGER.info("[宏执行] 调用函数: {} (后台执行: {}),函数包含 {} 个命令", funcName, func.isBackground, func.commands.size()); + + // 确保函数可以访问宏的所有函数定义(用于递归调用) + func.functions = macro.functions; + + if (func.isBackground) { + // 后台执行(不阻塞主线程) + new Thread(() -> { + LOGGER.info("[宏执行] 函数 {} 在后台线程开始执行", funcName); + executeFunctionCommands(func); + LOGGER.info("[宏执行] 函数 {} 在后台线程执行完成", funcName); + }, "MacroFunction-" + funcName).start(); + } else { + // 主线程执行 + LOGGER.info("[宏执行] 函数 {} 在主线程执行", funcName); + executeFunctionCommands(func); + } + } + + /** + * 执行函数命令 + */ + private void executeFunctionCommands(Function func) { + // 检查函数是否包含条件语句(需要循环执行) + boolean hasConditional = func.commands.stream() + .anyMatch(cmd -> cmd instanceof IfStatement); + + if (hasConditional) { + // 包含条件语句,需要循环执行 + LOGGER.info("[宏执行] 函数 {} 包含条件语句,将循环执行", func.name); + while (!stopped.get()) { + for (MacroCommand cmd : func.commands) { + if (stopped.get()) { + return; + } + try { + cmd.execute(this); + // 如果是条件语句,等待一小段时间再检查 + if (cmd instanceof IfStatement) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } catch (Exception e) { + LOGGER.error("[宏执行] 函数 {} 执行命令时出错", func.name, e); + } + } + // 每次循环之间等待一段时间 + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } else { + // 不包含条件语句,一次性执行完 + for (MacroCommand cmd : func.commands) { + if (stopped.get()) { + break; + } + try { + cmd.execute(this); + } catch (Exception e) { + LOGGER.error("[宏执行] 函数 {} 执行命令时出错", func.name, e); + } + } + } + } + + /** + * 检查命令是否是阻塞命令(需要等待执行完成) + */ + private boolean isBlockingCommand(String command) { + if (command == null || command.isEmpty()) { + return false; + } + String cmd = command.trim().toLowerCase(); + // 阻塞命令列表:goto, mine, explore, follow, farm 等需要等待完成的命令 + return cmd.startsWith("goto ") || + cmd.startsWith("mine ") || + cmd.startsWith("explore") || + cmd.startsWith("follow ") || + cmd.startsWith("farm"); + } + + /** + * 执行阻塞的 baritone 命令,等待执行完成 + */ + private void executeBaritoneCommandBlocking(String command) { + LOGGER.info("[宏执行] 执行阻塞 Baritone 命令: {}", command); + + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) { + LOGGER.warn("[宏执行] 无法执行 baritone 命令,玩家未初始化"); + return; + } + + try { + // 获取 Baritone 实例 + IBaritone baritone = BaritoneAPI.getProvider().getPrimaryBaritone(); + if (baritone == null) { + LOGGER.warn("[宏执行] Baritone 未加载,无法执行命令"); + return; + } + + // 解析命令以获取目标坐标(用于 goto 命令) + BlockPos targetPos = null; + String cmd = command.trim().toLowerCase(); + if (cmd.startsWith("goto ")) { + String[] parts = command.trim().split("\\s+"); + if (parts.length >= 4) { + try { + int x = Integer.parseInt(parts[1]); + int y = Integer.parseInt(parts[2]); + int z = Integer.parseInt(parts[3]); + targetPos = new BlockPos(x, y, z); + LOGGER.info("[宏执行] 解析到目标坐标: ({}, {}, {})", x, y, z); + } catch (NumberFormatException e) { + LOGGER.warn("[宏执行] 无法解析 goto 命令的坐标: {}", command); + } + } + } + + // 在主游戏线程中执行命令 + if (mc.isSameThread()) { + executeBaritoneCommandInternal(command); + } else { + final String cmdFinal = command; + mc.execute(() -> executeBaritoneCommandInternal(cmdFinal)); + + // 等待命令被提交 + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + + // 等待命令执行完成 + waitForBaritoneCommand(baritone, command, targetPos); + + } catch (Exception e) { + LOGGER.error("[宏执行] 执行阻塞 Baritone 命令时出错: {}", command, e); + e.printStackTrace(); + } finally { + // 命令执行完成,清除命令记录 + BaritoneTaskManager.getInstance().clearMacroCommand(macroName); + } + } + + /** + * 等待 Baritone 命令执行完成 + */ + private void waitForBaritoneCommand(IBaritone baritone, String command, BlockPos targetPos) { + LOGGER.info("[宏执行] 等待 Baritone 命令执行完成: {}", command); + + String cmd = command.trim().toLowerCase(); + + // 根据命令类型选择不同的等待策略 + if (cmd.startsWith("goto ")) { + // 等待路径查找完成 + try { + waitForPathfinding(baritone, targetPos); + } finally { + // 路径查找完成,清除命令记录 + BaritoneTaskManager.getInstance().clearMacroCommand(macroName); + } + } else if (cmd.startsWith("mine ")) { + // 等待挖矿完成(通常需要手动停止或挖完目标) + waitForMining(baritone); + } else { + // 默认等待策略:等待一小段时间 + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + LOGGER.info("[宏执行] Baritone 命令执行完成: {}", command); + } + + /** + * 等待路径查找完成 + * 通过检查玩家位置与目标位置的距离来判断是否到达 + */ + private void waitForPathfinding(IBaritone baritone, BlockPos targetPos) { + LOGGER.info("[宏执行] 等待路径查找完成,目标坐标: {}", targetPos); + + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + LOGGER.warn("[宏执行] 玩家未初始化,无法等待路径查找"); + return; + } + + int maxWaitTime = 300000; // 最大等待5分钟 + int checkInterval = 500; // 每500ms检查一次 + int waited = 0; + BlockPos lastPos = null; + int stableCount = 0; // 位置稳定计数 + final int TOLERANCE = 3; // 到达目标的容差(3格) + + while (waited < maxWaitTime && !stopped.get()) { + try { + BlockPos currentPos = mc.player.blockPosition(); + + // 如果有目标坐标,检查是否到达目标 + if (targetPos != null) { + double distance = Math.sqrt( + Math.pow(currentPos.getX() - targetPos.getX(), 2) + + Math.pow(currentPos.getY() - targetPos.getY(), 2) + + Math.pow(currentPos.getZ() - targetPos.getZ(), 2) + ); + + if (distance <= TOLERANCE) { + LOGGER.info("[宏执行] 已到达目标位置,距离: {} 格", String.format("%.2f", distance)); + // 等待一小段时间确认位置稳定 + Thread.sleep(1000); + if (mc.player.blockPosition().distSqr(targetPos) <= TOLERANCE * TOLERANCE) { + LOGGER.info("[宏执行] 路径查找已完成(已到达目标)"); + break; + } + } + } + + // 检查位置是否稳定(连续几次检查位置不变,说明已到达或停止) + if (lastPos != null && currentPos.equals(lastPos)) { + stableCount++; + // 如果位置稳定超过2秒(4次检查),认为已到达或停止 + if (stableCount >= 4) { + LOGGER.info("[宏执行] 路径查找已完成(位置已稳定)"); + break; + } + } else { + stableCount = 0; + } + + lastPos = currentPos; + + Thread.sleep(checkInterval); + waited += checkInterval; + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.warn("[宏执行] 等待路径查找时被中断"); + break; + } catch (Exception e) { + LOGGER.error("[宏执行] 等待路径查找时出错", e); + break; + } + } + + if (waited >= maxWaitTime) { + LOGGER.warn("[宏执行] 等待路径查找超时"); + } else { + LOGGER.info("[宏执行] 路径查找等待完成,耗时: {}ms", waited); + } + } + + /** + * 等待挖矿完成 + */ + private void waitForMining(IBaritone baritone) { + LOGGER.info("[宏执行] 等待挖矿完成..."); + + // 挖矿通常需要手动停止或挖完目标,这里等待一小段时间 + // 可以根据需要调整等待时间 + try { + Thread.sleep(2000); // 等待2秒,让挖矿开始 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // 注意:挖矿命令通常不会自动完成,需要手动停止或挖完目标 + // 这里只是等待挖矿开始,实际完成需要根据具体情况判断 + LOGGER.info("[宏执行] 挖矿命令已启动(注意:挖矿可能需要手动停止)"); + + // 挖矿命令会持续运行,直到被停止或完成 + // 命令记录会在宏停止或命令被替换时清除 + } + + /** + * 执行 baritone 命令(非阻塞) + * 直接使用 Baritone API 执行命令,而不是通过聊天消息 + */ + private void executeBaritoneCommand(String command) { + logToChatAndLogger("[宏执行] 准备执行 Baritone 命令: " + command); + LOGGER.info("[宏执行] 准备执行 Baritone 命令: {}", command); + + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + LOGGER.warn("[宏执行] 无法执行 baritone 命令,玩家未初始化"); + return; + } + + // 确保在主游戏线程中执行 + if (mc.isSameThread()) { + // 已经在主线程,直接执行 + executeBaritoneCommandInternal(command); + } else { + // 不在主线程,切换到主线程执行 + final String cmd = command; + mc.execute(() -> executeBaritoneCommandInternal(cmd)); + + // 等待一小段时间确保命令被提交 + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * 内部方法:实际执行 Baritone 命令 + */ + private void executeBaritoneCommandInternal(String command) { + try { + // 获取 Baritone 实例 + IBaritone baritone = BaritoneAPI.getProvider().getPrimaryBaritone(); + if (baritone == null) { + LOGGER.warn("[宏执行] Baritone 未加载,无法执行命令"); + return; + } + + LOGGER.info("[宏执行] 已获取 Baritone 实例,准备执行命令"); + + // 使用 Baritone 的命令管理器直接执行命令 + String fullCommand = command.trim(); + if (fullCommand.isEmpty()) { + LOGGER.warn("[宏执行] 命令为空"); + return; + } + + LOGGER.info("[宏执行] 执行 Baritone 命令: {}", fullCommand); + + // 执行命令 + baritone.getCommandManager().execute(fullCommand); + + logToChatAndLogger("[宏执行] ✓ 已通过 Baritone API 执行命令: " + fullCommand); + LOGGER.info("[宏执行] ✓ 已通过 Baritone API 执行命令: {}", fullCommand); + + } catch (Exception e) { + LOGGER.error("[宏执行] 执行 Baritone 命令时出错: {}", command, e); + e.printStackTrace(); + } + } + + /** + * 游戏刻更新 + */ + public void onTick() { + // 可以在这里检查时间条件等 + } + + /** + * 执行 wait 命令 + * wait: 一直阻塞直到宏结束 + * wait xs: 阻塞 x 秒 + * wait xm: 阻塞 x 分钟 + * wait xh: 阻塞 x 小时 + */ + public void executeWaitCommand(String content) { + if (stopped.get()) { + return; + } + + try { + content = content.trim(); + + // wait(无参数):一直阻塞直到宏结束 + if (content.isEmpty()) { + logToChatAndLogger("[宏执行] 执行 wait 命令,将一直阻塞直到宏结束"); + LOGGER.info("[宏执行] 执行 wait 命令,将一直阻塞直到宏结束"); + + // 循环检查是否停止,每100ms检查一次 + while (!stopped.get()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.warn("[宏执行] wait 命令被中断"); + break; + } + } + + logToChatAndLogger("[宏执行] wait 命令结束(宏已停止)"); + LOGGER.info("[宏执行] wait 命令结束(宏已停止)"); + return; + } + + // 解析时间参数 + long waitTimeMs = 0; + + // 匹配格式:数字 + s/m/h + java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("^(\\d+)([smh])$"); + java.util.regex.Matcher matcher = pattern.matcher(content); + + if (matcher.matches()) { + long value = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2); + + switch (unit) { + case "s": + waitTimeMs = value * 1000; // 秒 + break; + case "m": + waitTimeMs = value * 60 * 1000; // 分钟 + break; + case "h": + waitTimeMs = value * 60 * 60 * 1000; // 小时 + break; + default: + LOGGER.warn("[宏执行] wait 命令格式错误: {}", content); + logToChatAndLogger("[宏执行] wait 命令格式错误: " + content); + return; + } + + logToChatAndLogger(String.format("[宏执行] 执行 wait 命令,将阻塞 %d%s", value, unit)); + LOGGER.info("[宏执行] 执行 wait 命令,将阻塞 {}ms ({} {})", waitTimeMs, value, unit); + + // 阻塞指定时间,但每100ms检查一次是否停止 + long startTime = System.currentTimeMillis(); + while (!stopped.get() && (System.currentTimeMillis() - startTime) < waitTimeMs) { + try { + long remaining = waitTimeMs - (System.currentTimeMillis() - startTime); + Thread.sleep(Math.min(100, remaining)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.warn("[宏执行] wait 命令被中断"); + break; + } + } + + if (stopped.get()) { + logToChatAndLogger("[宏执行] wait 命令提前结束(宏已停止)"); + LOGGER.info("[宏执行] wait 命令提前结束(宏已停止)"); + } else { + logToChatAndLogger("[宏执行] wait 命令完成"); + LOGGER.info("[宏执行] wait 命令完成"); + } + } else { + LOGGER.warn("[宏执行] wait 命令格式错误: {},正确格式: wait、wait xs、wait xm、wait xh", content); + logToChatAndLogger("[宏执行] wait 命令格式错误: " + content + ",正确格式: wait、wait xs、wait xm、wait xh"); + } + + } catch (Exception e) { + LOGGER.error("[宏执行] 执行 wait 命令时出错", e); + logToChatAndLogger("[宏执行] 执行 wait 命令时出错: " + e.getMessage()); + } + } + + /** + * 执行 check 命令 + * 语法1: check me have (item = Pickaxe,type = diamond,quantity = 1), do #goto 0 0 0; + * 语法2: check me at = (0,0,0),do end; + * 语法3: check time = 11000,do /home; + */ + public void executeCheckCommand(CheckCommand cmd) { + if (stopped.get()) { + return; + } + + try { + boolean condition = false; + + switch (cmd.type) { + case ITEM: + condition = checkItem(cmd); + break; + case NOTHAVE: + condition = checkNotHaveItem(cmd); + break; + case POSITION: + condition = checkPosition(cmd); + break; + case TIME: + condition = checkTime(cmd); + break; + } + + if (condition) { + // 条件满足,执行动作 + logToChatAndLogger(String.format("[宏执行] check 条件满足,执行动作: %s", cmd.action)); + LOGGER.info("[宏执行] check 条件满足,执行动作: {}", cmd.action); + + // 执行动作(可能是 do 命令或其他) + String actionToExecute = cmd.action; + if (actionToExecute.startsWith("do ")) { + actionToExecute = actionToExecute.substring(3).trim(); + } + + // 检查是否是 end 命令(停止整个宏) + if (actionToExecute.equals("end")) { + logToChatAndLogger("[宏执行] check 条件满足,执行 end 命令,停止整个宏"); + LOGGER.info("[宏执行] check 条件满足,执行 end 命令,停止整个宏"); + stop(); + return; + } + + // 执行其他动作 + executeDoCommand(actionToExecute); + } else { + // 条件不满足,继续执行下一个命令 + logToChatAndLogger("[宏执行] check 条件不满足,继续执行下一个命令"); + LOGGER.info("[宏执行] check 条件不满足,继续执行下一个命令"); + } + + } catch (Exception e) { + LOGGER.error("[宏执行] 执行 check 命令时出错", e); + logToChatAndLogger("[宏执行] 执行 check 命令时出错: " + e.getMessage()); + } + } + + /** + * 执行 run 命令 + * 语法: run name = "回家" + * 立即启动指定的宏 + */ + public void executeRunCommand(String macroName) { + if (stopped.get()) { + return; + } + + try { + logToChatAndLogger(String.format("[宏执行] 执行 run 命令,启动宏: %s", macroName)); + LOGGER.info("[宏执行] 执行 run 命令,启动宏: {}", macroName); + + // 启动宏(独立执行,不阻塞当前宏) + BaritoneTaskManager.getInstance().startMacro(macroName); + + logToChatAndLogger(String.format("[宏执行] run 命令执行成功,已启动宏: %s", macroName)); + LOGGER.info("[宏执行] run 命令执行成功,已启动宏: {}", macroName); + + } catch (NotFanMacroFound e) { + LOGGER.error("[宏执行] run 命令失败: {}", e.getMessage()); + logToChatAndLogger("[宏执行] run 命令失败: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("[宏执行] 执行 run 命令时出错", e); + logToChatAndLogger("[宏执行] 执行 run 命令时出错: " + e.getMessage()); + } + } + + /** + * 检查物品 + */ + private boolean checkItem(CheckCommand cmd) { + try { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) { + return false; + } + + // 获取玩家背包 + net.minecraft.world.entity.player.Inventory inventory = mc.player.getInventory(); + if (inventory == null) { + return false; + } + + int totalCount = 0; + String itemNameLower = cmd.itemName.toLowerCase(); + + // 判断是否是工具检查(指定了 type 参数) + boolean isToolCheck = cmd.itemType != null && !cmd.itemType.isEmpty(); + + // 遍历所有物品槽位(包括主手、副手、背包) + for (int i = 0; i < inventory.getContainerSize(); i++) { + net.minecraft.world.item.ItemStack stack = inventory.getItem(i); + if (stack.isEmpty()) { + continue; + } + + net.minecraft.world.item.Item item = stack.getItem(); + String itemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(item).toString(); + String itemName = itemId.substring(itemId.indexOf(':') + 1); // 去掉命名空间 + String itemNameLowerActual = itemName.toLowerCase(); + + boolean nameMatches = false; + + if (isToolCheck) { + // 工具检查:支持部分匹配(如 "pickaxe" 匹配 "diamond_pickaxe") + nameMatches = itemNameLowerActual.equals(itemNameLower) || + itemNameLowerActual.contains(itemNameLower) || + itemNameLower.contains(itemNameLowerActual); + } else { + // 非工具检查:使用精确匹配(如 "diamond" 只匹配 "diamond",不匹配 "diamond_pickaxe") + nameMatches = itemNameLowerActual.equals(itemNameLower); + } + + if (nameMatches) { + // 如果指定了 type,检查物品类型 + if (isToolCheck) { + // 检查是否是工具类物品 + boolean isTool = isToolItem(item); + if (!isTool) { + // 不是工具类物品,但指定了 type,报错 + throw new IllegalArgumentException("物品类型错误: " + itemName + " 不是工具类物品,不能指定 type"); + } + + // 检查工具类型(材质) + String toolMaterial = getToolMaterial(item, itemName); + if (toolMaterial == null || !toolMaterial.equalsIgnoreCase(cmd.itemType)) { + continue; // 类型不匹配,跳过 + } + } else { + // 非工具检查:确保不是工具类物品(避免 "diamond" 匹配到 "diamond_pickaxe") + boolean isTool = isToolItem(item); + if (isTool) { + // 是工具类物品,但检查的是非工具物品,跳过 + continue; + } + } + + totalCount += stack.getCount(); + } + } + + boolean result = totalCount >= cmd.quantity; + logToChatAndLogger(String.format("[宏执行] 物品检查: 需要 %s (type=%s, quantity=%d), 找到数量=%d, 结果=%s", + cmd.itemName, cmd.itemType, cmd.quantity, totalCount, result)); + LOGGER.info("[宏执行] 物品检查: 需要 {} (type={}, quantity={}), 找到数量={}, 结果={}", + cmd.itemName, cmd.itemType, cmd.quantity, totalCount, result); + + return result; + + } catch (IllegalArgumentException e) { + LOGGER.error("[宏执行] 物品检查错误: {}", e.getMessage()); + logToChatAndLogger("[宏执行] 物品检查错误: " + e.getMessage()); + return false; + } catch (Exception e) { + LOGGER.error("[宏执行] 检查物品时出错", e); + return false; + } + } + + /** + * 检查是否是工具类物品 + */ + private boolean isToolItem(net.minecraft.world.item.Item item) { + try { + // 检查是否是工具类物品(镐、斧、铲、锄、剑等) + // 在 Minecraft 1.20.1 中,工具类物品通常继承自 DiggerItem 或 SwordItem + if (item instanceof net.minecraft.world.item.DiggerItem || + item instanceof net.minecraft.world.item.SwordItem) { + return true; + } + + // 也可以通过物品名称判断 + String itemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(item).toString(); + String itemName = itemId.substring(itemId.indexOf(':') + 1).toLowerCase(); + + // 工具类物品通常包含这些关键词 + String[] toolKeywords = {"pickaxe", "axe", "shovel", "hoe", "sword"}; + for (String keyword : toolKeywords) { + if (itemName.contains(keyword)) { + return true; + } + } + + return false; + } catch (Exception e) { + return false; + } + } + + /** + * 获取工具材质 + */ + private String getToolMaterial(net.minecraft.world.item.Item item, String itemName) { + try { + // 常见的工具材质(按优先级) + String[] materials = {"netherite", "diamond", "golden", "gold", "iron", "stone", "wooden", "wood"}; + + for (String material : materials) { + if (itemName.toLowerCase().contains(material)) { + // 标准化材质名称 + if (material.equals("gold")) { + return "golden"; + } else if (material.equals("wood")) { + return "wooden"; + } + return material; + } + } + + return null; + } catch (Exception e) { + return null; + } + } + + /** + * 检查没有物品(nothave) + * nothave 只允许 item 参数,不支持 type 和 quantity + * 使用精确匹配,避免 "diamond" 匹配到 "diamond_pickaxe" + */ + private boolean checkNotHaveItem(CheckCommand cmd) { + try { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) { + return true; // 无法检查,假设没有物品 + } + + // 获取玩家背包 + net.minecraft.world.entity.player.Inventory inventory = mc.player.getInventory(); + if (inventory == null) { + return true; // 无法检查,假设没有物品 + } + + String itemNameLower = cmd.itemName.toLowerCase(); + + // 遍历所有物品槽位(包括主手、副手、背包) + for (int i = 0; i < inventory.getContainerSize(); i++) { + net.minecraft.world.item.ItemStack stack = inventory.getItem(i); + if (stack.isEmpty()) { + continue; + } + + net.minecraft.world.item.Item item = stack.getItem(); + String itemId = net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(item).toString(); + String itemName = itemId.substring(itemId.indexOf(':') + 1); // 去掉命名空间 + String itemNameLowerActual = itemName.toLowerCase(); + + // nothave 使用精确匹配,避免 "diamond" 匹配到 "diamond_pickaxe" + // 例如:检查 "diamond" 时,只匹配 "diamond",不匹配 "diamond_pickaxe" + boolean nameMatches = itemNameLowerActual.equals(itemNameLower); + + if (nameMatches) { + // 找到了该物品,返回 false(有物品) + logToChatAndLogger(String.format("[宏执行] nothave 检查: 需要没有 %s, 但找到了 %s (数量=%d), 结果=false", + cmd.itemName, itemName, stack.getCount())); + LOGGER.info("[宏执行] nothave 检查: 需要没有 {}, 但找到了 {} (数量={}), 结果=false", + cmd.itemName, itemName, stack.getCount()); + return false; + } + } + + // 没有找到该物品,返回 true(没有物品) + logToChatAndLogger(String.format("[宏执行] nothave 检查: 需要没有 %s, 背包中没有, 结果=true", cmd.itemName)); + LOGGER.info("[宏执行] nothave 检查: 需要没有 {}, 背包中没有, 结果=true", cmd.itemName); + return true; + + } catch (Exception e) { + LOGGER.error("[宏执行] 检查 nothave 时出错", e); + return true; // 出错时假设没有物品 + } + } + + /** + * 检查位置 + */ + private boolean checkPosition(CheckCommand cmd) { + try { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) { + return false; + } + + net.minecraft.core.BlockPos pos = mc.player.blockPosition(); + int tolerance = 2; // ±2格容差 + + boolean result = Math.abs(pos.getX() - cmd.x) <= tolerance && + Math.abs(pos.getY() - cmd.y) <= tolerance && + Math.abs(pos.getZ() - cmd.z) <= tolerance; + + logToChatAndLogger(String.format("[宏执行] 位置检查: 目标=(%d,%d,%d), 当前位置=(%d,%d,%d), 容差=%d, 结果=%s", + cmd.x, cmd.y, cmd.z, pos.getX(), pos.getY(), pos.getZ(), tolerance, result)); + LOGGER.info("[宏执行] 位置检查: 目标=({},{},{}), 当前位置=({},{},{}), 容差={}, 结果={}", + cmd.x, cmd.y, cmd.z, pos.getX(), pos.getY(), pos.getZ(), tolerance, result); + + return result; + + } catch (Exception e) { + LOGGER.error("[宏执行] 检查位置时出错", e); + return false; + } + } + + /** + * 检查时间 + */ + private boolean checkTime(CheckCommand cmd) { + try { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) { + return false; + } + + long currentTime = BaritoneTaskManager.getCurrentTime(); + int timeTolerance = 50; // ±50 ticks tolerance (approx 2.5 seconds) + + boolean result = Math.abs(currentTime - cmd.time) <= timeTolerance; + + logToChatAndLogger(String.format("[宏执行] 时间检查: 目标=%d, 当前时间=%d, 容差=%d, 结果=%s", + cmd.time, currentTime, timeTolerance, result)); + LOGGER.info("[宏执行] 时间检查: 目标={}, 当前时间={}, 容差={}, 结果={}", + cmd.time, currentTime, timeTolerance, result); + + return result; + + } catch (Exception e) { + LOGGER.error("[宏执行] 检查时间时出错", e); + return false; + } + } + + /** + * 停止执行 + */ + public void stop() { + stopped.set(true); + // 清除命令记录 + BaritoneTaskManager.getInstance().clearMacroCommand(macroName); + } + + /** + * 检查是否已停止 + */ + public boolean isStopped() { + return stopped.get(); + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroParser.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroParser.java new file mode 100644 index 0000000..0318d48 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroParser.java @@ -0,0 +1,804 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 宏文件解析器 + * 解析宏文件并构建宏对象 + */ +public class MacroParser { + private static final Logger LOGGER = LogUtils.getLogger(); + + /** + * 解析宏文件 + */ + public static Macro parse(File file) throws IOException { + List lines = Files.readAllLines(file.toPath()); + Macro macro = new Macro(); + macro.name = file.getName().replace(".txt", ""); + + // 移除注释和空行 + List cleanLines = new ArrayList<>(); + for (String line : lines) { + String trimmed = line.trim(); + // 移除行尾注释 + int commentIndex = trimmed.indexOf("//"); + if (commentIndex >= 0) { + trimmed = trimmed.substring(0, commentIndex).trim(); + } + if (!trimmed.isEmpty()) { + cleanLines.add(trimmed); + } + } + + // 查找主函数入口 fan_main: + int mainIndex = -1; + for (int i = 0; i < cleanLines.size(); i++) { + if (cleanLines.get(i).equals("fan_main:") || cleanLines.get(i).startsWith("fan_main:")) { + mainIndex = i; + break; + } + } + + if (mainIndex >= 0) { + // 找到主函数入口 + // 第一步:先解析 fan_main: 之前的所有函数定义(如果有) + if (mainIndex > 0) { + parseMacroContent(cleanLines, macro, 0, mainIndex); + } + // 第二步:解析 fan_main: 之后的内容 + // 先解析所有函数定义,再解析主函数命令 + int mainStart = mainIndex + 1; + + // 先扫描并解析所有函数定义 + int i = mainStart; + while (i < cleanLines.size()) { + String line = cleanLines.get(i).trim(); + if (line.startsWith("fun ")) { + Function func = parseFunction(cleanLines, i, cleanLines.size()); + if (func != null) { + macro.functions.put(func.name, func); + LOGGER.info("[宏解析] 解析到函数: {} (后台执行: {})", func.name, func.isBackground); + i = func.endIndex; + continue; + } + } + i++; + } + + // 然后解析主函数命令(跳过函数定义) + Macro mainMacro = new Macro(); + i = mainStart; + while (i < cleanLines.size()) { + String line = cleanLines.get(i).trim(); + + // 跳过函数定义(已经在上面解析过了) + if (line.startsWith("fun ")) { + Function func = parseFunction(cleanLines, i, cleanLines.size()); + if (func != null) { + i = func.endIndex; + continue; + } + } + + // 解析主函数命令 + if (line.startsWith("if ")) { + IfStatement ifStmt = parseIfStatement(line); + i++; + + List ifCommands = new ArrayList<>(); + List elseCommands = new ArrayList<>(); + boolean inElse = false; + int depth = 1; + + while (i < cleanLines.size() && depth > 0) { + String currentLine = cleanLines.get(i).trim(); + + // 跳过函数定义 + if (currentLine.startsWith("fun ")) { + Function func = parseFunction(cleanLines, i, cleanLines.size()); + if (func != null) { + i = func.endIndex; + continue; + } + } + + if (currentLine.startsWith("if ")) { + depth++; + int nestedEnd = findNestedEnd(cleanLines, i, cleanLines.size()); + Macro nestedMacro = new Macro(); + int nextIndex = parseMacroContent(cleanLines, nestedMacro, i, nestedEnd); + if (inElse) { + elseCommands.addAll(nestedMacro.commands); + } else { + ifCommands.addAll(nestedMacro.commands); + } + i = nextIndex; + continue; + } else if (currentLine.equals("else") || currentLine.equals("else;")) { + if (depth == 1) { + inElse = true; + i++; + continue; + } + } else if (currentLine.equals("end;") || currentLine.equals("end")) { + depth--; + if (depth == 0) { + i++; + break; + } + } + + if (depth > 0) { + if (inElse && depth == 1) { + MacroCommand cmd = parseCommand(currentLine); + if (cmd != null) { + elseCommands.add(cmd); + } + } else if (!inElse && depth == 1) { + MacroCommand cmd = parseCommand(currentLine); + if (cmd != null) { + ifCommands.add(cmd); + } + } + } + i++; + } + + ifStmt.ifCommands = ifCommands; + ifStmt.elseCommands = elseCommands; + mainMacro.commands.add(ifStmt); + continue; + } + + MacroCommand cmd = parseCommand(line); + if (cmd != null) { + mainMacro.commands.add(cmd); + } + + i++; + } + + // 将主函数命令存储到 macro.mainCommands(如果存在)或 macro.commands + macro.commands.addAll(mainMacro.commands); + } else { + // 没有主函数入口,按原来的方式解析 + parseMacroContent(cleanLines, macro, 0, cleanLines.size()); + } + + LOGGER.info("[宏解析] 解析完成,共 {} 个命令,{} 个函数", macro.commands.size(), macro.functions.size()); + if (!macro.functions.isEmpty()) { + LOGGER.info("[宏解析] 函数列表: {}", macro.functions.keySet()); + } + if (!macro.commands.isEmpty()) { + LOGGER.info("[宏解析] 命令类型: {}", macro.commands.stream() + .map(cmd -> cmd.getClass().getSimpleName()) + .toList()); + } + + return macro; + } + + /** + * 解析宏内容(递归解析 if-else 和函数) + */ + private static int parseMacroContent(List lines, Macro macro, int startIndex, int endIndex) { + int i = startIndex; + while (i < endIndex && i < lines.size()) { + String line = lines.get(i); + + // 解析 if 语句 + if (line.startsWith("if ")) { + IfStatement ifStmt = parseIfStatement(line); + i++; // 跳过 if 行 + + // 解析 if 块 + List ifCommands = new ArrayList<>(); + List elseCommands = new ArrayList<>(); + boolean inElse = false; + int depth = 1; + + while (i < endIndex && i < lines.size() && depth > 0) { + String currentLine = lines.get(i).trim(); + + if (currentLine.startsWith("if ")) { + depth++; + // 嵌套的 if,递归解析 + int nestedEnd = findNestedEnd(lines, i, endIndex); + Macro nestedMacro = new Macro(); + int nextIndex = parseMacroContent(lines, nestedMacro, i, nestedEnd); + // 将嵌套的宏命令添加到当前块 + if (inElse) { + elseCommands.addAll(nestedMacro.commands); + } else { + ifCommands.addAll(nestedMacro.commands); + } + i = nextIndex; + continue; + } else if (currentLine.equals("else") || currentLine.equals("else;")) { + if (depth == 1) { + inElse = true; + i++; + continue; + } + } else if (currentLine.equals("end;") || currentLine.equals("end")) { + depth--; + if (depth == 0) { + i++; + break; + } + } + + if (depth > 0) { + if (inElse && depth == 1) { + // else 块中的命令 + MacroCommand cmd = parseCommand(currentLine); + if (cmd != null) { + elseCommands.add(cmd); + } + } else if (!inElse && depth == 1) { + // if 块中的命令 + MacroCommand cmd = parseCommand(currentLine); + if (cmd != null) { + ifCommands.add(cmd); + } + } + } + i++; + } + + ifStmt.ifCommands = ifCommands; + ifStmt.elseCommands = elseCommands; + macro.commands.add(ifStmt); + continue; + } + + // 解析函数定义 + if (line.startsWith("fun ")) { + Function func = parseFunction(lines, i, endIndex); + if (func != null) { + macro.functions.put(func.name, func); + i = func.endIndex; + continue; + } + } + + // 解析普通命令 + MacroCommand cmd = parseCommand(line); + if (cmd != null) { + macro.commands.add(cmd); + } + + i++; + } + + return i; + } + + /** + * 解析 if 语句 + */ + private static IfStatement parseIfStatement(String line) { + IfStatement stmt = new IfStatement(); + + // 解析 if me at = (x,y,z) + Pattern pattern = Pattern.compile("if\\s+me\\s+at\\s*=\\s*\\(\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*\\)"); + Matcher matcher = pattern.matcher(line); + if (matcher.matches()) { + stmt.type = IfStatement.Type.POSITION; + stmt.x = Integer.parseInt(matcher.group(1)); + stmt.y = Integer.parseInt(matcher.group(2)); + stmt.z = Integer.parseInt(matcher.group(3)); + return stmt; + } + + // 解析 if time = 11000 + pattern = Pattern.compile("if\\s+time\\s*=\\s*(\\d+)"); + matcher = pattern.matcher(line); + if (matcher.matches()) { + stmt.type = IfStatement.Type.TIME; + stmt.time = Long.parseLong(matcher.group(1)); + stmt.timeComparison = IfStatement.TimeComparison.EQUAL; + return stmt; + } + + // 解析 if time >= 11000 + pattern = Pattern.compile("if\\s+time\\s*>=\\s*(\\d+)"); + matcher = pattern.matcher(line); + if (matcher.matches()) { + stmt.type = IfStatement.Type.TIME; + stmt.time = Long.parseLong(matcher.group(1)); + stmt.timeComparison = IfStatement.TimeComparison.GREATER_EQUAL; + return stmt; + } + + // 解析 if time <= 11000 + pattern = Pattern.compile("if\\s+time\\s*<=\\s*(\\d+)"); + matcher = pattern.matcher(line); + if (matcher.matches()) { + stmt.type = IfStatement.Type.TIME; + stmt.time = Long.parseLong(matcher.group(1)); + stmt.timeComparison = IfStatement.TimeComparison.LESS_EQUAL; + return stmt; + } + + LOGGER.warn("[宏解析] 无法解析 if 语句: {}", line); + return stmt; + } + + /** + * 解析函数定义 + */ + private static Function parseFunction(List lines, int startIndex, int endIndex) { + String funcLine = lines.get(startIndex).trim(); + // fun name="name" type= &; + Pattern pattern = Pattern.compile("fun\\s+name\\s*=\\s*\"([^\"]+)\"\\s*(?:type\\s*=\\s*&)?\\s*;?"); + Matcher matcher = pattern.matcher(funcLine); + if (!matcher.matches()) { + return null; + } + + Function func = new Function(); + func.name = matcher.group(1); + func.isBackground = funcLine.contains("type=") && funcLine.contains("&"); + func.startIndex = startIndex + 1; + + // 找到函数结束位置(下一个 fun 或文件结束) + int i = startIndex + 1; + int depth = 0; + while (i < endIndex && i < lines.size()) { + String currentLine = lines.get(i).trim(); + if (currentLine.startsWith("fun ")) { + if (depth == 0) { + break; + } + } else if (currentLine.startsWith("if ")) { + depth++; + } else if (currentLine.equals("end;") || currentLine.equals("end")) { + depth--; + } + i++; + } + func.endIndex = i; + + // 解析函数内容 + Macro funcMacro = new Macro(); + parseMacroContent(lines, funcMacro, func.startIndex, func.endIndex); + func.commands = funcMacro.commands; + // 函数可以访问宏的所有函数定义(用于递归调用) + // 注意:这里需要传入父宏的函数定义,但函数定义在解析时可能还未完成 + // 所以需要在执行时从 MacroExecutor 传入 + + return func; + } + + /** + * 解析命令 + */ + private static MacroCommand parseCommand(String line) { + if (line.isEmpty() || line.equals("end;") || line.equals("end")) { + return null; + } + + // check 命令 + if (line.startsWith("check ")) { + String content = line.substring(6).trim(); + if (content.endsWith(";")) { + content = content.substring(0, content.length() - 1).trim(); + } + + CheckCommand cmd = parseCheckCommand(content); + if (cmd != null) { + return cmd; + } + } + + // wait 命令 + if (line.startsWith("wait")) { + String content = line.substring(4).trim(); + if (content.endsWith(";")) { + content = content.substring(0, content.length() - 1).trim(); + } + + WaitCommand cmd = new WaitCommand(); + cmd.content = content; // 可能是空(一直等待)或 "xs"、"xm"、"xh" 格式 + return cmd; + } + + // run 命令: run name = "回家" + if (line.startsWith("run ")) { + String content = line.substring(4).trim(); + if (content.endsWith(";")) { + content = content.substring(0, content.length() - 1).trim(); + } + + // 解析 run name = "回家" + Pattern runPattern = Pattern.compile("name\\s*=\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); + Matcher runMatcher = runPattern.matcher(content); + if (runMatcher.find()) { + String macroName = runMatcher.group(1); + RunCommand cmd = new RunCommand(); + cmd.macroName = macroName; + return cmd; + } else { + LOGGER.warn("[宏解析] run 命令格式错误: {}", content); + return null; + } + } + + // do #command args; + if (line.startsWith("do ")) { + String content = line.substring(3).trim(); + if (content.endsWith(";")) { + content = content.substring(0, content.length() - 1).trim(); + } + + DoCommand cmd = new DoCommand(); + cmd.content = content; + return cmd; + } + + return null; + } + + /** + * 解析 check 命令 + * 语法1: check me have (item = Pickaxe,type = diamond,quantity = 1), do #goto 0 0 0; + * 语法2: check me nothave (item = raw_iron), do #goto 0 0 0; + * 语法3: check me at = (0,0,0),do end; + * 语法4: check time = 11000,do /home; + */ + private static CheckCommand parseCheckCommand(String content) { + CheckCommand cmd = new CheckCommand(); + + try { + // 解析语法1: check me have (...), do ... + Pattern havePattern = Pattern.compile("me\\s+have\\s*\\(([^)]+)\\)\\s*,\\s*do\\s+(.+)", Pattern.CASE_INSENSITIVE); + Matcher haveMatcher = havePattern.matcher(content); + if (haveMatcher.matches()) { + cmd.type = CheckCommand.Type.ITEM; + String params = haveMatcher.group(1); + cmd.action = haveMatcher.group(2).trim(); + + // 解析参数: item = Pickaxe,type = diamond,quantity = 1 + Pattern paramPattern = Pattern.compile("(\\w+)\\s*=\\s*([^,]+)"); + Matcher paramMatcher = paramPattern.matcher(params); + while (paramMatcher.find()) { + String key = paramMatcher.group(1).trim().toLowerCase(); + String value = paramMatcher.group(2).trim(); + + if (key.equals("item")) { + cmd.itemName = value; + } else if (key.equals("type")) { + cmd.itemType = value; + } else if (key.equals("quantity")) { + try { + cmd.quantity = Integer.parseInt(value); + if (cmd.quantity < 1 || cmd.quantity > 64) { + LOGGER.warn("[宏解析] check 命令 quantity 超出范围 (1-64): {}", cmd.quantity); + return null; + } + } catch (NumberFormatException e) { + LOGGER.warn("[宏解析] check 命令 quantity 格式错误: {}", value); + return null; + } + } + } + + if (cmd.itemName == null) { + LOGGER.warn("[宏解析] check 命令缺少 item 参数"); + return null; + } + if (cmd.quantity == null) { + cmd.quantity = 1; // 默认数量为1 + } + + return cmd; + } + + // 解析语法2: check me nothave (item = raw_iron), do ... + Pattern notHavePattern = Pattern.compile("me\\s+nothave\\s*\\(([^)]+)\\)\\s*,\\s*do\\s+(.+)", Pattern.CASE_INSENSITIVE); + Matcher notHaveMatcher = notHavePattern.matcher(content); + if (notHaveMatcher.matches()) { + cmd.type = CheckCommand.Type.NOTHAVE; + String params = notHaveMatcher.group(1); + cmd.action = notHaveMatcher.group(2).trim(); + + // 解析参数: item = raw_iron (nothave 只允许 item 参数) + Pattern paramPattern = Pattern.compile("(\\w+)\\s*=\\s*([^,]+)"); + Matcher paramMatcher = paramPattern.matcher(params); + while (paramMatcher.find()) { + String key = paramMatcher.group(1).trim().toLowerCase(); + String value = paramMatcher.group(2).trim(); + + if (key.equals("item")) { + cmd.itemName = value; + } else { + // nothave 只允许 item 参数,其他参数报错 + LOGGER.warn("[宏解析] check nothave 命令只允许 item 参数,不允许: {}", key); + return null; + } + } + + if (cmd.itemName == null) { + LOGGER.warn("[宏解析] check nothave 命令缺少 item 参数"); + return null; + } + + return cmd; + } + + // 解析语法3: check me at = (x,y,z),do ... + Pattern atPattern = Pattern.compile("me\\s+at\\s*=\\s*\\(\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*\\)\\s*,\\s*do\\s+(.+)", Pattern.CASE_INSENSITIVE); + Matcher atMatcher = atPattern.matcher(content); + if (atMatcher.matches()) { + cmd.type = CheckCommand.Type.POSITION; + cmd.x = Integer.parseInt(atMatcher.group(1)); + cmd.y = Integer.parseInt(atMatcher.group(2)); + cmd.z = Integer.parseInt(atMatcher.group(3)); + cmd.action = atMatcher.group(4).trim(); + return cmd; + } + + // 解析语法4: check time = 11000,do ... + Pattern timePattern = Pattern.compile("time\\s*=\\s*(\\d+)\\s*,\\s*do\\s+(.+)", Pattern.CASE_INSENSITIVE); + Matcher timeMatcher = timePattern.matcher(content); + if (timeMatcher.matches()) { + cmd.type = CheckCommand.Type.TIME; + cmd.time = Long.parseLong(timeMatcher.group(1)); + cmd.action = timeMatcher.group(2).trim(); + return cmd; + } + + LOGGER.warn("[宏解析] check 命令格式错误: {}", content); + return null; + + } catch (Exception e) { + LOGGER.error("[宏解析] 解析 check 命令时出错: {}", content, e); + return null; + } + } + + /** + * 查找嵌套的 end + */ + private static int findNestedEnd(List lines, int startIndex, int endIndex) { + int depth = 1; + int i = startIndex + 1; + while (i < endIndex && i < lines.size() && depth > 0) { + String line = lines.get(i); + if (line.startsWith("if ")) { + depth++; + } else if (line.equals("end;") || line.equals("end")) { + depth--; + } + if (depth > 0) { + i++; + } + } + return i; + } +} + +/** + * 宏对象 + */ +class Macro { + String name; + List commands = new ArrayList<>(); + Map functions = new HashMap<>(); +} + +/** + * 函数对象 + */ +class Function { + String name; + boolean isBackground; + List commands = new ArrayList<>(); + Map functions = new HashMap<>(); // 函数可以访问宏的所有函数定义(用于递归调用) + int startIndex; + int endIndex; +} + +/** + * 命令接口 + */ +interface MacroCommand { + void execute(MacroExecutor executor); +} + +/** + * Do 命令 + */ +class DoCommand implements MacroCommand { + String content; + + @Override + public void execute(MacroExecutor executor) { + executor.executeDoCommand(content); + } +} + +/** + * Wait 命令 + */ +class WaitCommand implements MacroCommand { + String content; // 空字符串表示一直等待,否则是 "xs"、"xm"、"xh" 格式 + + @Override + public void execute(MacroExecutor executor) { + executor.executeWaitCommand(content); + } +} + +/** + * Check 命令 + */ +class CheckCommand implements MacroCommand { + enum Type { + ITEM, // 物品检查 (have) + NOTHAVE, // 没有物品检查 (nothave) + POSITION, // 位置检查 + TIME // 时间检查 + } + + Type type; + String action; // 条件满足时执行的动作 + + // 物品检查参数 + String itemName; // 物品名称,如 "Pickaxe" + String itemType; // 物品类型,如 "diamond"(仅工具类物品) + Integer quantity; // 数量 (1-64) + + // 位置检查参数 + int x, y, z; + + // 时间检查参数 + long time; + + @Override + public void execute(MacroExecutor executor) { + executor.executeCheckCommand(this); + } +} + +/** + * Run 命令 + */ +class RunCommand implements MacroCommand { + String macroName; // 要启动的宏名称 + + @Override + public void execute(MacroExecutor executor) { + executor.executeRunCommand(macroName); + } +} + +/** + * If 语句 + */ +class IfStatement implements MacroCommand { + enum Type { + POSITION, + TIME + } + + enum TimeComparison { + EQUAL, + GREATER_EQUAL, + LESS_EQUAL + } + + Type type; + int x, y, z; + long time; + TimeComparison timeComparison = TimeComparison.EQUAL; + List ifCommands = new ArrayList<>(); + List elseCommands = new ArrayList<>(); + + @Override + public void execute(MacroExecutor executor) { + boolean condition = false; + + if (type == Type.POSITION) { + var pos = BaritoneTaskManager.getPlayerPosition(); + if (pos != null) { + // 坐标检测容错范围:±2格 + int tolerance = 2; + condition = Math.abs(pos.getX() - x) <= tolerance + && Math.abs(pos.getY() - y) <= tolerance + && Math.abs(pos.getZ() - z) <= tolerance; + String coordInfo = String.format("[宏执行] 坐标检查: 目标=(%d,%d,%d), 当前位置=(%d,%d,%d), 容差=%d, 结果=%s", + x, y, z, pos.getX(), pos.getY(), pos.getZ(), tolerance, condition); + logToChatAndLogger(coordInfo); + com.mojang.logging.LogUtils.getLogger().info("[宏执行] 坐标检查: 目标=({},{},{}), 当前位置=({},{},{}), 容差={}, 结果={}", + x, y, z, pos.getX(), pos.getY(), pos.getZ(), tolerance, condition); + } else { + com.mojang.logging.LogUtils.getLogger().warn("[宏执行] 无法获取玩家位置"); + } + } else if (type == Type.TIME) { + long currentTime = BaritoneTaskManager.getCurrentTime(); + + // 时间检查容差:±50刻(约2.5秒) + // 因为服务器时间可能不会精确匹配,需要容差范围 + long tolerance = 50; + + switch (timeComparison) { + case EQUAL: + // 使用容差范围检查相等 + condition = Math.abs(currentTime - time) <= tolerance; + break; + case GREATER_EQUAL: + condition = currentTime >= (time - tolerance); + break; + case LESS_EQUAL: + condition = currentTime <= (time + tolerance); + break; + } + String timeInfo = String.format("[宏执行] 时间检查: 目标=%d, 当前时间=%d, 容差=%d, 结果=%s", + time, currentTime, tolerance, condition); + logToChatAndLogger(timeInfo); + com.mojang.logging.LogUtils.getLogger().info("[宏执行] 时间检查: 目标={}, 当前时间={}, 容差={}, 结果={}", + time, currentTime, tolerance, condition); + } + + List commandsToExecute = condition ? ifCommands : elseCommands; + String ifInfo = String.format("[宏执行] IfStatement 条件=%s, 将执行 %d 个命令 (if分支: %d, else分支: %d)", + condition, commandsToExecute.size(), ifCommands.size(), elseCommands.size()); + logToChatAndLogger(ifInfo); + com.mojang.logging.LogUtils.getLogger().info("[宏执行] IfStatement 条件={}, 将执行 {} 个命令 (if分支: {}, else分支: {})", + condition, commandsToExecute.size(), ifCommands.size(), elseCommands.size()); + + // 保存停止状态,以便在执行分支后恢复 + boolean wasStopped = executor.isStopped(); + + // 执行分支中的命令 + for (MacroCommand cmd : commandsToExecute) { + if (executor.isStopped()) { + com.mojang.logging.LogUtils.getLogger().info("[宏执行] IfStatement 分支执行被停止(遇到 end 命令)"); + break; + } + String branchCmdInfo = "[宏执行] 执行 IfStatement 分支中的命令: " + cmd.getClass().getSimpleName(); + logToChatAndLogger(branchCmdInfo); + com.mojang.logging.LogUtils.getLogger().info("[宏执行] 执行 IfStatement 分支中的命令: {}", cmd.getClass().getSimpleName()); + cmd.execute(executor); + } + + // 重要:IfStatement 执行完分支后,应该继续执行后续命令 + // 只有当分支中执行了 do end; 时,才会停止整个宏 + // 但这里我们不恢复停止状态,因为 end 命令应该停止整个宏 + // 如果分支中没有 end,执行会自然继续到下一个命令 + logToChatAndLogger("[宏执行] IfStatement 分支执行完成,继续执行后续命令"); + com.mojang.logging.LogUtils.getLogger().info("[宏执行] IfStatement 分支执行完成,继续执行后续命令"); + } + + /** + * 同时记录到日志和聊天框(工具方法) + */ + private static void logToChatAndLogger(String message) { + com.mojang.logging.LogUtils.getLogger().info(message); + + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.player != null) { + // 在主游戏线程中显示消息 + mc.execute(() -> { + if (mc.player != null) { + // 使用系统消息,不会发送到服务器 + mc.player.sendSystemMessage(Component.literal("§7[宏] §r" + message)); + } + }); + } + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroWebServer.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroWebServer.java new file mode 100644 index 0000000..437d236 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/MacroWebServer.java @@ -0,0 +1,623 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import net.minecraft.client.Minecraft; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; + +/** + * 宏管理 Web 服务器 + * 端口: 8079 + * 提供网页界面来管理宏文件 + */ +public class MacroWebServer { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int HTTP_PORT = 8079; + private static HttpServer httpServer; + private static boolean isRunning = false; + + /** + * 初始化 Web 服务器 + */ + public static void initialize() { + if (isRunning) { + return; + } + + try { + // 先尝试停止可能存在的旧服务器 + stop(); + + httpServer = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + httpServer.createContext("/", new WebInterfaceHandler()); + httpServer.createContext("/api/list", new ListMacrosHandler()); + httpServer.createContext("/api/execute", new ExecuteMacroHandler()); + httpServer.createContext("/api/stop", new StopMacroHandler()); + httpServer.createContext("/api/status", new StatusHandler()); + httpServer.setExecutor(Executors.newCachedThreadPool()); + httpServer.start(); + isRunning = true; + + LOGGER.info("[宏管理] Web 服务器已启动,访问 http://localhost:{} 管理宏", HTTP_PORT); + + } catch (java.net.BindException e) { + LOGGER.error("[宏管理] 端口 {} 已被占用", HTTP_PORT); + isRunning = false; + } catch (IOException e) { + LOGGER.error("[宏管理] 启动失败: {}", e.getMessage(), e); + isRunning = false; + } + } + + /** + * 停止 Web 服务器 + */ + public static void stop() { + if (httpServer != null && isRunning) { + try { + httpServer.stop(0); + LOGGER.info("[宏管理] Web 服务器已停止"); + } catch (Exception e) { + LOGGER.error("[宏管理] 停止失败: {}", e.getMessage()); + } + httpServer = null; + isRunning = false; + } + } + + /** + * Web 界面处理器 + */ + private static class WebInterfaceHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + String html = generateHTML(); + byte[] bytes = html.getBytes(StandardCharsets.UTF_8); + + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + + private String generateHTML() { + return """ + + + + + + 宏管理 - Baritone 任务管理器 + + + +
+

🎮 Baritone 宏管理

+

管理你的自动化任务宏文件

+ +
+
+
📋 宏文件列表
+ +
+
+
+
正在加载宏文件...
+
+
+
+ + + + + """; + } + + private void sendError(HttpExchange exchange, int code, String message) throws IOException { + String response = "{\"error\":\"" + escapeJson(message) + "\"}"; + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(code, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + + private String escapeJson(String str) { + if (str == null) return ""; + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } + + /** + * 列出宏文件 API + */ + private static class ListMacrosHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + try { + BaritoneTaskManager manager = BaritoneTaskManager.getInstance(); + if (manager.getMacroFolder() == null) { + manager.initialize(); + } + + File macroFolder = manager.getMacroFolder(); + if (macroFolder == null || !macroFolder.exists()) { + sendResponse(exchange, 200, "{\"success\":false,\"message\":\"宏文件夹不存在\"}"); + return; + } + + File[] files = macroFolder.listFiles((dir, name) -> name.endsWith(".txt")); + List macroNames = new ArrayList<>(); + if (files != null) { + for (File file : files) { + macroNames.add(file.getName().replace(".txt", "")); + } + } + + String response = String.format("{\"success\":true,\"macros\":%s}", + macroNames.stream() + .map(name -> "\"" + escapeJson(name) + "\"") + .reduce((a, b) -> a + "," + b) + .map(list -> "[" + list + "]") + .orElse("[]")); + + sendResponse(exchange, 200, response); + + } catch (Exception e) { + LOGGER.error("[宏管理] 列出宏文件时出错", e); + sendResponse(exchange, 500, "{\"success\":false,\"message\":\"" + escapeJson(e.getMessage()) + "\"}"); + } + } + } + + /** + * 执行宏 API + */ + private static class ExecuteMacroHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + String macroName = null; + try { + String query = exchange.getRequestURI().getQuery(); + if (query != null) { + for (String param : query.split("&")) { + String[] parts = param.split("=", 2); + if (parts.length == 2 && parts[0].equals("macro")) { + macroName = java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8); + break; + } + } + } + + if (macroName == null || macroName.isEmpty()) { + sendResponse(exchange, 400, "{\"success\":false,\"message\":\"缺少 macro 参数\"}"); + return; + } + + BaritoneTaskManager manager = BaritoneTaskManager.getInstance(); + if (manager.getMacroFolder() == null) { + manager.initialize(); + } + + File macroFile = new File(manager.getMacroFolder(), macroName + ".txt"); + if (!macroFile.exists()) { + sendResponse(exchange, 404, "{\"success\":false,\"message\":\"宏文件不存在: " + escapeJson(macroName) + ".txt\"}"); + return; + } + + // 如果宏已经在运行,先停止 + if (manager.isMacroRunning(macroName)) { + manager.stopMacro(macroName); + } + + // 加载并启动宏 + Macro macro = MacroParser.parse(macroFile); + manager.loadMacro(macroName, macro); + manager.startMacro(macroName); + + sendResponse(exchange, 200, "{\"success\":true,\"message\":\"宏已启动: " + escapeJson(macroName) + "\"}"); + LOGGER.info("[宏管理] 通过 Web 界面启动宏: {}", macroName); + + } catch (NotFanMacroFound e) { + String errorMacroName = macroName != null ? macroName : e.getMessage(); + LOGGER.error("[宏管理] 宏不存在: {}", errorMacroName); + sendResponse(exchange, 404, "{\"success\":false,\"message\":\"NotFanMacroFound: " + escapeJson(errorMacroName) + "\"}"); + } catch (Exception e) { + LOGGER.error("[宏管理] 执行宏时出错", e); + sendResponse(exchange, 500, "{\"success\":false,\"message\":\"" + escapeJson(e.getMessage()) + "\"}"); + } + } + } + + /** + * 停止宏 API + */ + private static class StopMacroHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + try { + String query = exchange.getRequestURI().getQuery(); + String macroName = null; + if (query != null) { + for (String param : query.split("&")) { + String[] parts = param.split("=", 2); + if (parts.length == 2 && parts[0].equals("macro")) { + macroName = java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8); + break; + } + } + } + + if (macroName == null || macroName.isEmpty()) { + sendResponse(exchange, 400, "{\"success\":false,\"message\":\"缺少 macro 参数\"}"); + return; + } + + BaritoneTaskManager manager = BaritoneTaskManager.getInstance(); + if (manager.isMacroRunning(macroName)) { + manager.stopMacro(macroName); + sendResponse(exchange, 200, "{\"success\":true,\"message\":\"宏已停止: " + escapeJson(macroName) + "\"}"); + LOGGER.info("[宏管理] 通过 Web 界面停止宏: {}", macroName); + } else { + sendResponse(exchange, 200, "{\"success\":false,\"message\":\"宏未在运行: " + escapeJson(macroName) + "\"}"); + } + + } catch (Exception e) { + LOGGER.error("[宏管理] 停止宏时出错", e); + sendResponse(exchange, 500, "{\"success\":false,\"message\":\"" + escapeJson(e.getMessage()) + "\"}"); + } + } + } + + /** + * 状态查询 API + */ + private static class StatusHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + try { + BaritoneTaskManager manager = BaritoneTaskManager.getInstance(); + java.util.Set macroNames = manager.getMacroNames(); + java.util.Map running = new java.util.HashMap<>(); + + for (String macroName : macroNames) { + running.put(macroName, manager.isMacroRunning(macroName)); + } + + String runningJson = running.entrySet().stream() + .map(e -> "\"" + escapeJson(e.getKey()) + "\":" + e.getValue()) + .reduce((a, b) -> a + "," + b) + .map(s -> "{" + s + "}") + .orElse("{}"); + + String response = "{\"success\":true,\"running\":" + runningJson + "}"; + sendResponse(exchange, 200, response); + + } catch (Exception e) { + LOGGER.error("[宏管理] 查询状态时出错", e); + sendResponse(exchange, 500, "{\"success\":false,\"message\":\"" + escapeJson(e.getMessage()) + "\"}"); + } + } + } + + private static void sendResponse(HttpExchange exchange, int code, String response) throws IOException { + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(code, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + + private static void sendError(HttpExchange exchange, int code, String message) throws IOException { + sendResponse(exchange, code, "{\"error\":\"" + escapeJson(message) + "\"}"); + } + + private static String escapeJson(String str) { + if (str == null) return ""; + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/NotFanMacroFound.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/NotFanMacroFound.java new file mode 100644 index 0000000..6264a16 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/NotFanMacroFound.java @@ -0,0 +1,12 @@ +package com.xiaofan.autosaveforforge; + +/** + * 宏未找到异常 + * 当尝试启动不存在的宏时抛出 + */ +public class NotFanMacroFound extends Exception { + public NotFanMacroFound(String macroName) { + super("宏未找到: " + macroName); + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/PerformanceController.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/PerformanceController.java new file mode 100644 index 0000000..1bb03fe --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/PerformanceController.java @@ -0,0 +1,286 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.gui.screens.PauseScreen; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.lwjgl.glfw.GLFW; +import org.slf4j.Logger; + +/** + * 性能控制器 + * Home 键:切换分辨率到 320x240(再次按下恢复) + * End 键:切换帧率限制到 10FPS(开关模式,原版最低限制) + * F9 键:释放/锁定鼠标(开关模式) + */ +@Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) +public class PerformanceController { + private static final Logger LOGGER = LogUtils.getLogger(); + + // 分辨率控制 + private static boolean isLowResolutionMode = false; + private static int originalWidth = -1; + private static int originalHeight = -1; + private static final int LOW_RES_WIDTH = 320; + private static final int LOW_RES_HEIGHT = 240; + + // 帧率控制 + private static boolean isLowFPSMode = false; + private static int originalMaxFPS = -1; + private static final int LOW_FPS = 10; // 原版最低只能限制到 10 FPS + + // 鼠标控制 + private static boolean isMouseReleased = false; + + // 按键状态跟踪(避免重复触发) + private static boolean homeKeyPressed = false; + private static boolean endKeyPressed = false; + private static boolean f9KeyPressed = false; + + /** + * 监听客户端 Tick 事件来检测按键 + */ + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + return; + } + + // 如果屏幕打开(GUI),重置按键状态(但 F9 键仍然可以工作) + if (mc.screen != null) { + homeKeyPressed = false; + endKeyPressed = false; + // F9 键在 GUI 打开时也可以工作,用于锁定鼠标 + } + + // 检测 Home 键 + long windowHandle = mc.getWindow().getWindow(); + boolean homeKeyDown = GLFW.glfwGetKey(windowHandle, GLFW.GLFW_KEY_HOME) == GLFW.GLFW_PRESS; + if (homeKeyDown && !homeKeyPressed) { + homeKeyPressed = true; + toggleResolution(mc); + } else if (!homeKeyDown) { + homeKeyPressed = false; + } + + // 检测 End 键 + boolean endKeyDown = GLFW.glfwGetKey(windowHandle, GLFW.GLFW_KEY_END) == GLFW.GLFW_PRESS; + if (endKeyDown && !endKeyPressed) { + endKeyPressed = true; + toggleFPSLimit(mc); + } else if (!endKeyDown) { + endKeyPressed = false; + } + + // 检测 F9 键(无论是否在 GUI 中都可以使用) + boolean f9KeyDown = GLFW.glfwGetKey(windowHandle, GLFW.GLFW_KEY_F9) == GLFW.GLFW_PRESS; + if (f9KeyDown && !f9KeyPressed) { + f9KeyPressed = true; + toggleMouseGrab(mc); + } else if (!f9KeyDown) { + f9KeyPressed = false; + } + } + + /** + * 切换分辨率 + */ + private static void toggleResolution(Minecraft mc) { + mc.execute(() -> { + try { + if (!isLowResolutionMode) { + // 保存原始分辨率 + if (originalWidth == -1 || originalHeight == -1) { + originalWidth = mc.getWindow().getWidth(); + originalHeight = mc.getWindow().getHeight(); + LOGGER.info("[性能控制] 保存原始分辨率: {}x{}", originalWidth, originalHeight); + } + + // 切换到低分辨率 + mc.getWindow().setWindowed(LOW_RES_WIDTH, LOW_RES_HEIGHT); + isLowResolutionMode = true; + LOGGER.info("[性能控制] 分辨率已切换到: {}x{}", LOW_RES_WIDTH, LOW_RES_HEIGHT); + + // 显示提示消息 + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 分辨率已切换到 320x240")); + } + } else { + // 恢复原始分辨率 + if (originalWidth > 0 && originalHeight > 0) { + mc.getWindow().setWindowed(originalWidth, originalHeight); + LOGGER.info("[性能控制] 分辨率已恢复到: {}x{}", originalWidth, originalHeight); + + // 显示提示消息 + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 分辨率已恢复到 " + originalWidth + "x" + originalHeight)); + } + } else { + // 如果没有保存的原始分辨率,使用默认值 + mc.getWindow().setWindowed(854, 480); + LOGGER.info("[性能控制] 分辨率已恢复到默认值: 854x480"); + + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 分辨率已恢复到默认值")); + } + } + isLowResolutionMode = false; + } + } catch (Exception e) { + LOGGER.error("[性能控制] 切换分辨率失败: {}", e.getMessage(), e); + } + }); + } + + /** + * 切换帧率限制 + */ + private static void toggleFPSLimit(Minecraft mc) { + mc.execute(() -> { + try { + if (!isLowFPSMode) { + // 保存原始帧率限制 + if (originalMaxFPS == -1) { + originalMaxFPS = mc.options.framerateLimit().get(); + LOGGER.info("[性能控制] 保存原始帧率限制: {} FPS", originalMaxFPS); + } + + // 切换到低帧率(原版最低只能到 10 FPS) + mc.options.framerateLimit().set(LOW_FPS); + isLowFPSMode = true; + LOGGER.info("[性能控制] 帧率限制已切换到: {} FPS (原版最低限制)", LOW_FPS); + + // 显示提示消息 + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 帧率限制已切换到 " + LOW_FPS + " FPS (原版最低限制)")); + } + } else { + // 恢复原始帧率限制 + if (originalMaxFPS > 0) { + mc.options.framerateLimit().set(originalMaxFPS); + LOGGER.info("[性能控制] 帧率限制已恢复到: {} FPS", originalMaxFPS); + + // 显示提示消息 + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 帧率限制已恢复到 " + originalMaxFPS + " FPS")); + } + } else { + // 如果没有保存的原始帧率限制,使用默认值(通常是 60) + mc.options.framerateLimit().set(60); + LOGGER.info("[性能控制] 帧率限制已恢复到默认值: 60 FPS"); + + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 帧率限制已恢复到默认值")); + } + } + isLowFPSMode = false; + } + } catch (Exception e) { + LOGGER.error("[性能控制] 切换帧率限制失败: {}", e.getMessage(), e); + } + }); + } + + /** + * 获取当前分辨率模式 + */ + public static boolean isLowResolutionMode() { + return isLowResolutionMode; + } + + /** + * 获取当前帧率限制模式 + */ + public static boolean isLowFPSMode() { + return isLowFPSMode; + } + + /** + * 切换鼠标锁定状态 + */ + private static void toggleMouseGrab(Minecraft mc) { + mc.execute(() -> { + try { + if (!isMouseReleased) { + // 释放鼠标(显示鼠标光标) + mc.mouseHandler.releaseMouse(); + isMouseReleased = true; + LOGGER.info("[性能控制] 鼠标已释放(显示鼠标光标)"); + + // 显示提示消息 + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 鼠标已释放(显示鼠标光标)")); + } + } else { + // 锁定鼠标(隐藏鼠标光标,进入游戏模式) + mc.mouseHandler.grabMouse(); + isMouseReleased = false; + LOGGER.info("[性能控制] 鼠标已锁定(隐藏鼠标光标)"); + + // 显示提示消息 + if (mc.player != null) { + mc.player.sendSystemMessage(net.minecraft.network.chat.Component.literal("§a[性能控制] 鼠标已锁定(隐藏鼠标光标)")); + } + } + } catch (Exception e) { + LOGGER.error("[性能控制] 切换鼠标锁定状态失败: {}", e.getMessage(), e); + } + }); + } + + /** + * 获取当前鼠标是否已释放 + */ + public static boolean isMouseReleased() { + return isMouseReleased; + } + + /** + * 窗口焦点事件监听器 + * 防止窗口失去焦点时自动显示暂停菜单 + */ + @Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) + public static class WindowFocusHandler { + private static boolean wasWindowFocused = true; + + /** + * 监听屏幕打开事件,如果是因为窗口失去焦点而打开的暂停菜单,则关闭它 + */ + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onScreenOpen(ScreenEvent.Opening event) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + return; + } + + // 检查是否是暂停菜单 + if (event.getNewScreen() instanceof PauseScreen) { + // 检查窗口是否失去焦点 + boolean isWindowFocused = GLFW.glfwGetWindowAttrib(mc.getWindow().getWindow(), GLFW.GLFW_FOCUSED) == GLFW.GLFW_TRUE; + + // 如果窗口失去焦点,且之前是焦点状态,则阻止显示暂停菜单 + if (!isWindowFocused && wasWindowFocused && mc.level != null && !mc.isPaused()) { + event.setCanceled(true); + LOGGER.debug("[性能控制] 阻止因窗口失去焦点而显示暂停菜单"); + } + } + + // 更新窗口焦点状态 + wasWindowFocused = GLFW.glfwGetWindowAttrib(mc.getWindow().getWindow(), GLFW.GLFW_FOCUSED) == GLFW.GLFW_TRUE; + } + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ServerInfoAPI.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ServerInfoAPI.java new file mode 100644 index 0000000..1af2615 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/ServerInfoAPI.java @@ -0,0 +1,1008 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +// import org.json.JSONObject; // 改为手动解析 JSON,避免运行时依赖问题 +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Forge 版本的服务器信息 API + * 端口: 2000 + * 端点: + * - GET /need_server_info - 获取在线玩家信息 + * - GET /get_server_last_message - 获取最后一条聊天消息 + * - POST /send_message_to_server - 发送消息到服务器 + */ +public class ServerInfoAPI { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int HTTP_PORT = 2000; + private static HttpServer httpServer; + private static boolean isRunning = false; + + // 聊天消息队列(线程安全) + private static final ConcurrentLinkedQueue chatMessageQueue = new ConcurrentLinkedQueue<>(); + + /** + * 初始化HTTP服务器和消息监听 + */ + public static void initialize() { + if (isRunning) { + return; + } + + // 注册消息监听器 + MinecraftForge.EVENT_BUS.register(new ChatMessageListener()); + + try { + // 先尝试停止可能存在的旧服务器 + stop(); + + httpServer = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + httpServer.createContext("/need_server_info", new ServerInfoHandler()); + httpServer.createContext("/get_server_last_message", new LastMessageHandler()); + httpServer.createContext("/send_message_to_server", new SendMessageHandler()); + httpServer.createContext("/debug", new DebugHandler()); // 添加调试界面 + httpServer.setExecutor(Executors.newCachedThreadPool()); + httpServer.start(); + isRunning = true; + + LOGGER.info("[服务器信息API] HTTP服务器已启动 (端口: {})", HTTP_PORT); + + } catch (java.net.BindException e) { + LOGGER.error("[服务器信息API] 端口 {} 已被占用", HTTP_PORT); + isRunning = false; + } catch (IOException e) { + LOGGER.error("[服务器信息API] 启动失败: {}", e.getMessage(), e); + isRunning = false; + } + } + + /** + * 停止HTTP服务器 + */ + public static void stop() { + if (httpServer != null && isRunning) { + try { + httpServer.stop(0); + LOGGER.info("[服务器信息API] HTTP服务器已停止"); + } catch (Exception e) { + LOGGER.error("[服务器信息API] 停止失败: {}", e.getMessage()); + } + httpServer = null; + isRunning = false; + } + } + + /** + * 聊天消息监听器 + * 监听 Forge 的 ClientChatReceivedEvent 事件 + */ + @Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) + public static class ChatMessageListener { + @SubscribeEvent + public static void onChatReceived(ClientChatReceivedEvent event) { + Component messageComponent = event.getMessage(); + if (messageComponent == null) { + return; + } + + // 将 Component 转换为字符串 + String messageText = messageComponent.getString(); + if (messageText == null || messageText.trim().isEmpty()) { + return; + } + + LOGGER.debug("[服务器信息API] [CHAT事件] 收到原始聊天消息: {}", messageText); + + // 过滤系统消息(如"加入了游戏"、"离开服务器"等) + if (isSystemMessage(messageText)) { + LOGGER.debug("[服务器信息API] [CHAT事件] 过滤系统消息: {}", messageText); + return; + } + + // 去掉 [System] [CHAT] 或 [Not Secure] [CHAT] 前缀(如果存在) + String cleanMessage = removeSystemPrefix(messageText); + + // 如果清理后的消息为空,跳过 + if (cleanMessage == null || cleanMessage.trim().isEmpty()) { + LOGGER.debug("[服务器信息API] [CHAT事件] 清理后消息为空,跳过"); + return; + } + + // 直接将所有可见的服务器消息加入队列(不再检查格式,不再解析) + chatMessageQueue.offer(cleanMessage.trim()); + LOGGER.info("[服务器信息API] [CHAT事件] 消息已加入队列: {}", cleanMessage.trim()); + } + } + + /** + * 判断是否是系统消息(需要过滤掉的消息) + * @param message 消息内容 + * @return true表示是系统消息,需要过滤 + */ + private static boolean isSystemMessage(String message) { + if (message == null) { + return true; + } + + String lower = message.toLowerCase(); + + // 过滤系统提示消息 + return lower.contains("加入了游戏") || + lower.contains("离开服务器") || + lower.contains("进入了服务器") || + lower.contains("退出了服务器") || + lower.contains("joined the game") || + lower.contains("left the game"); + } + + /** + * 判断消息是否包含方括号格式(用于识别自定义消息格式) + * @param message 消息内容 + * @return true表示消息中包含至少一个方括号块,可能是自定义格式 + */ + private static boolean hasBracketFormat(String message) { + if (message == null || message.trim().isEmpty()) { + return false; + } + + String trimmed = message.trim(); + // 检查是否包含至少一个完整的方括号块 [xxx] + Pattern bracketPattern = Pattern.compile("\\[[^\\]]+\\]"); + return bracketPattern.matcher(trimmed).find(); + } + + /** + * 去掉消息中的 [System] [CHAT] 或 [Not Secure] [CHAT] 前缀 + * @param message 原始消息 + * @return 去掉前缀后的消息 + */ + private static String removeSystemPrefix(String message) { + if (message == null) { + return null; + } + + String cleaned = message; + + // 匹配 [System] [CHAT] 前缀并去掉 + Pattern pattern = Pattern.compile("^\\s*\\[System\\]\\s*\\[CHAT\\]\\s*(.+)$", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(cleaned); + if (matcher.matches()) { + cleaned = matcher.group(1).trim(); + LOGGER.debug("[服务器信息API] 去掉 [System] [CHAT] 前缀: {} -> {}", message, cleaned); + } + + // 匹配 [Not Secure] [CHAT] 前缀并去掉 + pattern = Pattern.compile("^\\s*\\[Not Secure\\]\\s*\\[CHAT\\]\\s*(.+)$", Pattern.CASE_INSENSITIVE); + matcher = pattern.matcher(cleaned); + if (matcher.matches()) { + cleaned = matcher.group(1).trim(); + LOGGER.debug("[服务器信息API] 去掉 [Not Secure] [CHAT] 前缀: {} -> {}", message, cleaned); + } + + // 匹配 [System] 前缀并去掉 + pattern = Pattern.compile("^\\s*\\[System\\]\\s*(.+)$", Pattern.CASE_INSENSITIVE); + matcher = pattern.matcher(cleaned); + if (matcher.matches()) { + cleaned = matcher.group(1).trim(); + LOGGER.debug("[服务器信息API] 去掉 [System] 前缀: {} -> {}", message, cleaned); + } + + return cleaned; + } + + /** + * 解析自定义消息格式 + * 支持三种格式: + * 1. [xxx] [xxx] [xxx] 实际消息内容(三个方括号) + * 2. [xxx] [xxx] 实际消息内容(两个方括号) + * 3. [xxx] 实际消息内容(一个方括号) + * + * 格式示例: + * - [塞勒涅盟约] [雅典维亚城邦] [幸运戴师OVO] xiaofan: 114514 + * - [信息1] [信息2] 消息内容 + * - [信息] 消息内容 + * - [无头衔] MelloFurry落地过猛 + * - 6 + * + * @param rawMessage 原始消息 + * @return 解析后的聊天内容(去掉方括号前缀后的实际消息),如果解析失败返回null + */ + private static String parseCustomMessageFormat(String rawMessage) { + if (rawMessage == null || rawMessage.trim().isEmpty()) { + LOGGER.debug("[服务器信息API] 解析消息: 输入为空"); + return null; + } + + String trimmed = rawMessage.trim(); + LOGGER.debug("[服务器信息API] 开始解析消息: {}", trimmed); + + // 格式1: [xxx] [xxx] [xxx] 实际消息内容(三个方括号) + Pattern pattern = Pattern.compile( + "^\\s*\\[([^\\]]+)\\]\\s*\\[([^\\]]+)\\]\\s*\\[([^\\]]+)\\]\\s*(.+)$" + ); + + Matcher matcher = pattern.matcher(trimmed); + if (matcher.matches()) { + String info1 = matcher.group(1); + String info2 = matcher.group(2); + String info3 = matcher.group(3); + String chatContent = matcher.group(4).trim(); + + LOGGER.debug("[服务器信息API] ✓ 匹配三括号格式 - [{}] [{}] [{}] -> {}", info1, info2, info3, chatContent); + + return chatContent; + } + + // 格式2: [xxx] [xxx] 实际消息内容(两个方括号) + pattern = Pattern.compile( + "^\\s*\\[([^\\]]+)\\]\\s*\\[([^\\]]+)\\]\\s*(.+)$" + ); + matcher = pattern.matcher(trimmed); + if (matcher.matches()) { + String info1 = matcher.group(1); + String info2 = matcher.group(2); + String chatContent = matcher.group(3).trim(); + + LOGGER.debug("[服务器信息API] ✓ 匹配两括号格式 - [{}] [{}] -> {}", info1, info2, chatContent); + + return chatContent; + } + + // 格式3: [xxx] 实际消息内容(一个方括号) + pattern = Pattern.compile( + "^\\s*\\[([^\\]]+)\\]\\s*(.+)$" + ); + matcher = pattern.matcher(trimmed); + if (matcher.matches()) { + String info = matcher.group(1); + String chatContent = matcher.group(2).trim(); + + LOGGER.debug("[服务器信息API] ✓ 匹配单括号格式 - [{}] -> {}", info, chatContent); + + return chatContent; + } + + // 格式4: 消息内容(尖括号格式,如 6) + pattern = Pattern.compile( + "^\\s*<([^>]+)>\\s*(.+)$" + ); + matcher = pattern.matcher(trimmed); + if (matcher.matches()) { + String info = matcher.group(1); + String chatContent = matcher.group(2).trim(); + + LOGGER.debug("[服务器信息API] ✓ 匹配尖括号格式 - <{}> -> {}", info, chatContent); + + return chatContent; + } + + // 如果都不匹配,返回null(不加入队列) + LOGGER.debug("[服务器信息API] ✗ 消息格式不匹配任何模式,返回null: {}", trimmed); + return null; + } + + /** + * 服务器信息处理器 + */ + private static class ServerInfoHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + // 只处理GET请求 + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + try { + Minecraft mc = Minecraft.getInstance(); + + // 检查客户端是否连接到服务器 + boolean isConnected = mc != null + && mc.getConnection() != null + && mc.player != null; + + if (!isConnected) { + // 未连接状态,返回502 + sendError(exchange, 502, "Forge client is not connected to server"); + return; + } + + // 获取在线玩家详细信息 + List playerInfoList = getOnlinePlayers(mc); + + // 构建JSON响应 + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append("{\"online_players\":["); + + for (int i = 0; i < playerInfoList.size(); i++) { + PlayerInfo info = playerInfoList.get(i); + if (i > 0) { + jsonBuilder.append(","); + } + jsonBuilder.append("{"); + jsonBuilder.append("\"username\":\"").append(escapeJson(info.username)).append("\","); + jsonBuilder.append("\"latency\":").append(info.latency); + jsonBuilder.append("}"); + } + + jsonBuilder.append("],\"count\":").append(playerInfoList.size()).append("}"); + + String jsonResponse = jsonBuilder.toString(); + + // 发送响应 + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(200, jsonResponse.getBytes(StandardCharsets.UTF_8).length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(jsonResponse.getBytes(StandardCharsets.UTF_8)); + } + + } catch (Exception e) { + LOGGER.error("[服务器信息API] 处理请求时出错: {}", e.getMessage(), e); + sendError(exchange, 500, "Internal server error"); + } + } + + /** + * 玩家信息数据类 + */ + private static class PlayerInfo { + String username; + int latency; + + PlayerInfo(String username, int latency) { + this.username = username; + this.latency = latency; + } + } + + /** + * 获取在线玩家详细信息(包括用户名和延迟) + */ + private List getOnlinePlayers(Minecraft mc) { + List playerInfoList = new ArrayList<>(); + + try { + ClientPacketListener connection = mc.getConnection(); + if (connection != null) { + Collection players = connection.getOnlinePlayers(); + if (players != null) { + // 遍历所有玩家条目 + for (net.minecraft.client.multiplayer.PlayerInfo entry : players) { + if (entry != null) { + String username = "未知"; + int latency = 0; + + try { + // 获取玩家名称 + if (entry.getProfile() != null && entry.getProfile().getName() != null) { + username = entry.getProfile().getName(); + } else { + // 尝试从 DisplayName 获取 + Component displayName = entry.getTabListDisplayName(); + if (displayName != null) { + username = displayName.getString(); + } + } + + // 获取延迟(以毫秒为单位) + latency = entry.getLatency(); + + } catch (Exception e) { + LOGGER.error("[服务器信息API] 获取玩家信息时出错: {}", e.getMessage()); + } + + playerInfoList.add(new PlayerInfo(username, latency)); + } + } + } + } + } catch (Exception e) { + LOGGER.error("[服务器信息API] 获取玩家列表时出错: {}", e.getMessage(), e); + } + + return playerInfoList; + } + + /** + * 发送错误响应 + */ + private void sendError(HttpExchange exchange, int statusCode, String message) throws IOException { + String errorResponse = String.format("{\"error\":\"%s\"}", escapeJson(message)); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(statusCode, errorResponse.getBytes(StandardCharsets.UTF_8).length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(errorResponse.getBytes(StandardCharsets.UTF_8)); + } + } + + /** + * 转义JSON字符串 + */ + private String escapeJson(String str) { + if (str == null) { + return ""; + } + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } + + /** + * 最后一条消息处理器 + * 返回聊天框的最后一条消息,每次返回后清空,确保不会重复获取 + */ + private static class LastMessageHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + // 只处理GET请求 + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + try { + // 从队列中取出并移除一条消息(FIFO) + String message = chatMessageQueue.poll(); + + String response; + if (message != null) { + // 返回消息内容 + response = String.format("{\"message\":\"%s\"}", escapeJson(message)); + LOGGER.debug("[服务器信息API] 返回消息: {}", message); + } else { + // 队列为空,返回null + response = "{\"message\":null}"; + LOGGER.debug("[服务器信息API] 队列为空,返回null"); + } + + // 发送响应 + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(200, response.getBytes(StandardCharsets.UTF_8).length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + + } catch (Exception e) { + LOGGER.error("[服务器信息API] 处理请求时出错: {}", e.getMessage(), e); + sendError(exchange, 500, "Internal server error"); + } + } + + /** + * 发送错误响应 + */ + private void sendError(HttpExchange exchange, int statusCode, String message) throws IOException { + String errorResponse = String.format("{\"error\":\"%s\"}", escapeJson(message)); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(statusCode, errorResponse.getBytes(StandardCharsets.UTF_8).length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(errorResponse.getBytes(StandardCharsets.UTF_8)); + } + } + + /** + * 转义JSON字符串 + */ + private String escapeJson(String str) { + if (str == null) { + return ""; + } + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } + + /** + * 发送消息到服务器处理器 + * 接收POST请求,将消息发送到Minecraft服务器 + * 注意:Forge 不需要使用 /c 命令,直接发送消息即可 + */ + private static class SendMessageHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + // 只处理POST请求 + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + try { + // 读取请求体 + InputStream requestBody = exchange.getRequestBody(); + String requestText = new String(requestBody.readAllBytes(), StandardCharsets.UTF_8); + + LOGGER.debug("[服务器信息API] 收到发送消息请求: {}", requestText); + + // 手动解析JSON(避免外部依赖) + String qqId = parseJsonString(requestText, "qq_id", ""); + String message = parseJsonString(requestText, "message", ""); + String source = parseJsonString(requestText, "source", "qq"); // 默认为qq,支持kook + + // 验证参数 + if (message == null || message.trim().isEmpty()) { + sendError(exchange, 400, "message字段不能为空"); + return; + } + + // 发送消息到Minecraft服务器(Forge 直接发送,不需要 /c 命令) + boolean success = sendMessageToServer(qqId, message, source); + + if (success) { + String response = String.format("{\"status\":\"success\",\"message\":\"消息已发送\",\"qq_id\":\"%s\",\"source\":\"%s\"}", escapeJson(qqId), escapeJson(source)); + sendResponse(exchange, 200, response); + String sourceLabel = "kook".equalsIgnoreCase(source) ? "KOOK" : "QQ"; + LOGGER.info("[服务器信息API] 消息已发送: {}消息:用户:{}: {} ({}: {})", sourceLabel, qqId, message, sourceLabel, qqId); + } else { + String response = "{\"status\":\"error\",\"message\":\"客户端未连接到服务器\"}"; + sendResponse(exchange, 502, response); + LOGGER.warn("[服务器信息API] 发送失败: 客户端未连接"); + } + + } catch (Exception e) { + LOGGER.error("[服务器信息API] 处理请求时出错: {}", e.getMessage(), e); + sendError(exchange, 500, "Internal server error: " + e.getMessage()); + } + } + + /** + * 发送消息到Minecraft服务器 + * @param qqId 用户昵称(作为qq_id) + * @param message 消息内容 + * @param source 消息来源("qq" 或 "kook") + * @return 是否发送成功 + */ + private boolean sendMessageToServer(String qqId, String message, String source) { + Minecraft mc = Minecraft.getInstance(); + + // 检查客户端是否连接到服务器 + if (mc == null || mc.getConnection() == null || mc.player == null) { + return false; + } + + try { + // 根据来源构建不同的消息格式 + String finalMessage; + if ("kook".equalsIgnoreCase(source)) { + // KOOK消息格式:KOOK消息:用户:用户名: 消息内容 + finalMessage = "KOOK消息:用户:" + qqId + ": " + message; + } else { + // QQ消息格式:QQ消息:用户:用户名: 消息内容 + finalMessage = "QQ消息:用户:" + qqId + ": " + message; + } + + // 在主游戏线程中执行(Forge 直接发送消息,不需要 /c 命令) + mc.execute(() -> { + try { + // 直接发送消息(不需要 /c 命令) + // Forge 1.20.1 使用 connection.sendChat() 方法,接受 String 参数 + if (mc.player != null && mc.getConnection() != null) { + mc.getConnection().sendChat(finalMessage); + LOGGER.debug("[服务器信息API] 已发送消息: {}", finalMessage); + } + } catch (Exception e) { + LOGGER.error("[服务器信息API] 发送消息时出错: {}", e.getMessage(), e); + } + }); + + return true; + } catch (Exception e) { + LOGGER.error("[服务器信息API] 执行消息发送时出错: {}", e.getMessage(), e); + return false; + } + } + + /** + * 发送成功响应 + */ + private void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(statusCode, response.getBytes(StandardCharsets.UTF_8).length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + } + + /** + * 发送错误响应 + */ + private void sendError(HttpExchange exchange, int statusCode, String message) throws IOException { + String errorResponse = String.format("{\"error\":\"%s\"}", escapeJson(message)); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(statusCode, errorResponse.getBytes(StandardCharsets.UTF_8).length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(errorResponse.getBytes(StandardCharsets.UTF_8)); + } + } + + /** + * 手动解析JSON字符串中的字段值 + * @param json JSON字符串 + * @param key 要获取的键 + * @param defaultValue 默认值 + * @return 字段值,如果不存在则返回默认值 + */ + private String parseJsonString(String json, String key, String defaultValue) { + if (json == null || json.trim().isEmpty()) { + return defaultValue; + } + + // 查找 "key": "value" 或 "key": "value" + String pattern = "\"" + key + "\"\\s*:\\s*\"([^\"]*)\""; + java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern); + java.util.regex.Matcher m = p.matcher(json); + + if (m.find()) { + return m.group(1); + } + + // 如果没有找到,返回默认值 + return defaultValue; + } + + /** + * 转义JSON字符串 + */ + private String escapeJson(String str) { + if (str == null) { + return ""; + } + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } + + /** + * 调试界面处理器 + * 提供 Web 界面用于测试 /send_message_to_server 接口 + */ + private static class DebugHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method not allowed"); + return; + } + + String html = generateDebugHTML(); + byte[] bytes = html.getBytes(StandardCharsets.UTF_8); + + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + + private String generateDebugHTML() { + return """ + + + + + + 服务器信息 API 调试界面 + + + +
+

🔧 服务器信息 API 调试界面

+ +
+
测试 /send_message_to_server 接口
+
+
+ + +
+
+ + +
+
+ + +
+ + +
+
等待操作...
+
+
+ +
+
可用 API 端点
+
+ GET + /need_server_info +
获取在线玩家信息
+
+
+ GET + /get_server_last_message +
获取最后一条聊天消息
+
+
+ POST + /send_message_to_server +
发送消息到服务器
+
+
+ GET + /debug +
调试界面(当前页面)
+
+
+
+ + + + + """; + } + + private void sendError(HttpExchange exchange, int code, String message) throws IOException { + String response = "{\"error\":\"" + escapeJson(message) + "\",\"code\":" + code + "}"; + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(code, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } + + private String escapeJson(String str) { + if (str == null) { + return ""; + } + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/StatusHttpServer.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/StatusHttpServer.java new file mode 100644 index 0000000..9ef9047 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/StatusHttpServer.java @@ -0,0 +1,715 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; + +/** + * HTTP 服务器,用于显示当前游戏状态 + * 端口: 8083 + */ +public class StatusHttpServer { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int HTTP_PORT = 8083; + private static HttpServer httpServer; + private static boolean isRunning = false; + + // 持久连接相关 + private static boolean persistentConnection = false; + private static String lastServerAddress = null; + private static String lastProxyAddress = null; // 保存上次使用的代理地址 + private static long lastDisconnectTime = 0; + private static final long RECONNECT_DELAY = 60000; // 1分钟(超大型模组服需要更长时间加载) + + /** + * 游戏状态枚举 + */ + public enum GameStatus { + LOADING("加载中"), + TITLE_SCREEN("标题屏幕"), + SINGLE_PLAYER("单人游戏"), + MULTIPLAYER("服务器模式"); + + private final String displayName; + + GameStatus(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + } + + /** + * 获取当前游戏状态 + */ + public static GameStatusInfo getCurrentStatus() { + Minecraft mc = Minecraft.getInstance(); + + if (mc == null) { + return new GameStatusInfo(GameStatus.LOADING, null, null); + } + + // 检查是否在标题屏幕 + if (mc.screen instanceof TitleScreen) { + return new GameStatusInfo(GameStatus.TITLE_SCREEN, null, null); + } + + // 检查是否在游戏中 + if (mc.level != null) { + // 检查是否是多人游戏 + if (mc.getConnection() != null && mc.getConnection().getConnection() != null) { + ServerData serverData = mc.getCurrentServer(); + String serverAddress = serverData != null ? serverData.ip : "未知服务器"; + return new GameStatusInfo(GameStatus.MULTIPLAYER, serverAddress, null); + } else { + // 单人游戏 + String worldName = mc.getSingleplayerServer() != null ? + mc.getSingleplayerServer().getWorldData().getLevelName() : "未知世界"; + return new GameStatusInfo(GameStatus.SINGLE_PLAYER, null, worldName); + } + } + + // 默认状态:加载中 + return new GameStatusInfo(GameStatus.LOADING, null, null); + } + + /** + * 游戏状态信息 + */ + public static class GameStatusInfo { + public final GameStatus status; + public final String serverAddress; + public final String worldName; + + public GameStatusInfo(GameStatus status, String serverAddress, String worldName) { + this.status = status; + this.serverAddress = serverAddress; + this.worldName = worldName; + } + } + + /** + * 初始化 HTTP 服务器 + */ + public static void initialize() { + if (isRunning) { + return; + } + + try { + // 先尝试停止可能存在的旧服务器 + stop(); + + httpServer = HttpServer.create(new InetSocketAddress(HTTP_PORT), 0); + httpServer.createContext("/", new WebInterfaceHandler()); + httpServer.createContext("/status", new StatusHandler()); + httpServer.createContext("/connect", new ConnectHandler()); + httpServer.createContext("/disconnect", new DisconnectHandler()); + httpServer.createContext("/togglePersistent", new TogglePersistentHandler()); + httpServer.setExecutor(Executors.newCachedThreadPool()); + httpServer.start(); + isRunning = true; + + // 注册自动重连监听器 + MinecraftForge.EVENT_BUS.register(new AutoReconnectListener()); + + LOGGER.info("[状态服务器] HTTP服务器已启动 (端口: {})", HTTP_PORT); + LOGGER.info("[状态服务器] 访问 http://localhost:{} 查看游戏状态", HTTP_PORT); + + } catch (java.net.BindException e) { + LOGGER.error("[状态服务器] 端口 {} 已被占用", HTTP_PORT); + isRunning = false; + } catch (IOException e) { + LOGGER.error("[状态服务器] 启动失败: {}", e.getMessage(), e); + isRunning = false; + } + } + + /** + * 停止 HTTP 服务器 + */ + public static void stop() { + if (httpServer != null && isRunning) { + try { + httpServer.stop(0); + LOGGER.info("[状态服务器] HTTP服务器已停止"); + } catch (Exception e) { + LOGGER.error("[状态服务器] 停止失败: {}", e.getMessage()); + } + httpServer = null; + isRunning = false; + } + } + + /** + * Web 界面处理器 + */ + private static class WebInterfaceHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method Not Allowed"); + return; + } + + String html = generateHTML(); + sendResponse(exchange, html, "text/html; charset=utf-8"); + } + } + + /** + * 状态 API 处理器 + */ + private static class StatusHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"GET".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method Not Allowed"); + return; + } + + GameStatusInfo status = getCurrentStatus(); + StringBuilder json = new StringBuilder(); + json.append("{"); + json.append("\"status\":\"").append(status.status.name()).append("\","); + json.append("\"displayName\":\"").append(escapeJson(status.status.getDisplayName())).append("\","); + json.append("\"persistentConnection\":").append(persistentConnection); + + if (status.serverAddress != null) { + json.append(",\"serverAddress\":\"").append(escapeJson(status.serverAddress)).append("\""); + } + + if (status.worldName != null) { + json.append(",\"worldName\":\"").append(escapeJson(status.worldName)).append("\""); + } + + json.append("}"); + + sendResponse(exchange, json.toString(), "application/json; charset=utf-8"); + } + } + + /** + * 连接服务器处理器 + */ + private static class ConnectHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method Not Allowed"); + return; + } + + try { + // 读取请求体 + InputStream requestBody = exchange.getRequestBody(); + String body = new String(requestBody.readAllBytes(), StandardCharsets.UTF_8); + + // 解析服务器地址和代理地址 + String serverAddress = null; + String proxyAddress = null; + + if (body.contains("address=")) { + String addressPart = body.substring(body.indexOf("address=") + 8); + if (addressPart.contains("&")) { + serverAddress = addressPart.substring(0, addressPart.indexOf("&")); + } else { + serverAddress = addressPart; + } + serverAddress = java.net.URLDecoder.decode(serverAddress, StandardCharsets.UTF_8); + } + + if (body.contains("proxy=")) { + String proxyPart = body.substring(body.indexOf("proxy=") + 6); + if (proxyPart.contains("&")) { + proxyAddress = proxyPart.substring(0, proxyPart.indexOf("&")); + } else { + proxyAddress = proxyPart; + } + proxyAddress = java.net.URLDecoder.decode(proxyAddress, StandardCharsets.UTF_8); + if (proxyAddress.trim().isEmpty()) { + proxyAddress = null; + } + } + + if (serverAddress == null || serverAddress.trim().isEmpty()) { + sendError(exchange, 400, "服务器地址不能为空"); + return; + } + + boolean success = connectToServer(serverAddress, proxyAddress); + if (success) { + sendResponse(exchange, "{\"success\":true,\"message\":\"正在连接服务器: " + escapeJson(serverAddress) + "\"}", "application/json; charset=utf-8"); + } else { + sendError(exchange, 500, "连接失败"); + } + } catch (Exception e) { + LOGGER.error("处理连接请求失败: {}", e.getMessage(), e); + sendError(exchange, 500, "服务器错误: " + e.getMessage()); + } + } + } + + /** + * 断开连接处理器 + */ + private static class DisconnectHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method Not Allowed"); + return; + } + + // 停止自动重连 + persistentConnection = false; + lastServerAddress = null; + lastDisconnectTime = 0; + + boolean success = disconnectFromServer(); + if (success) { + sendResponse(exchange, "{\"success\":true,\"message\":\"已断开连接\"}", "application/json; charset=utf-8"); + } else { + sendError(exchange, 500, "断开连接失败"); + } + } + } + + /** + * 切换持久连接处理器 + */ + private static class TogglePersistentHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!"POST".equals(exchange.getRequestMethod())) { + sendError(exchange, 405, "Method Not Allowed"); + return; + } + + persistentConnection = !persistentConnection; + String message = persistentConnection ? "持久连接已启用" : "持久连接已禁用"; + + sendResponse(exchange, "{\"success\":true,\"persistentConnection\":" + persistentConnection + ",\"message\":\"" + message + "\"}", "application/json; charset=utf-8"); + } + } + + /** + * 连接服务器 + * @param address 服务器地址 + * @param proxyAddress 代理服务器地址(可选,如果为null则直接连接) + */ + private static boolean connectToServer(String address, String proxyAddress) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + return false; + } + + try { + lastServerAddress = address; + lastProxyAddress = proxyAddress; + + // 如果提供了代理地址,设置系统属性以使用 SOCKS5 代理 + if (proxyAddress != null && !proxyAddress.trim().isEmpty()) { + // 解析代理地址 + String proxyHost; + int proxyPort; + String[] proxyParts = proxyAddress.split(":"); + if (proxyParts.length == 2) { + proxyHost = proxyParts[0]; + proxyPort = Integer.parseInt(proxyParts[1]); + } else if (proxyParts.length == 1) { + proxyHost = proxyAddress; + proxyPort = 7000; // 默认端口 + } else { + LOGGER.error("[连接] 代理地址格式错误: {}", proxyAddress); + return false; + } + + // 设置系统属性以使用 SOCKS5 代理 + System.setProperty("socksProxyHost", proxyHost); + System.setProperty("socksProxyPort", String.valueOf(proxyPort)); + LOGGER.info("[连接] 已设置 SOCKS5 代理: {}:{}", proxyHost, proxyPort); + } else { + // 清除代理设置 + System.clearProperty("socksProxyHost"); + System.clearProperty("socksProxyPort"); + LOGGER.info("[连接] 已清除代理设置,直接连接"); + } + + final String finalAddress = address; + mc.execute(() -> { + try { + // 解析目标服务器地址 + String[] addressParts = finalAddress.split(":"); + String targetHost = addressParts[0]; + int targetPort = addressParts.length > 1 ? Integer.parseInt(addressParts[1]) : 25565; + + ServerAddress serverAddress = ServerAddress.parseString(finalAddress); + // ServerData 中保存实际的目标服务器地址 + ServerData serverData = new ServerData("自定义服务器", finalAddress, false); + + ConnectScreen.startConnecting( + mc.screen instanceof TitleScreen ? (TitleScreen) mc.screen : new TitleScreen(), + mc, + serverAddress, + serverData, + false + ); + + if (proxyAddress != null && !proxyAddress.trim().isEmpty()) { + LOGGER.info("[连接] 正在通过 SOCKS5 代理 {} 连接到服务器: {}", proxyAddress, finalAddress); + } else { + LOGGER.info("[连接] 正在连接到服务器: {}", finalAddress); + } + } catch (Exception e) { + LOGGER.error("连接服务器失败: {}", e.getMessage(), e); + } + }); + return true; + } catch (Exception e) { + LOGGER.error("连接服务器异常: {}", e.getMessage(), e); + return false; + } + } + + /** + * 断开服务器连接 + */ + private static boolean disconnectFromServer() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + return false; + } + + try { + mc.execute(() -> { + try { + // 断开当前连接 + if (mc.getConnection() != null && mc.getConnection().getConnection() != null) { + mc.getConnection().getConnection().disconnect(net.minecraft.network.chat.Component.literal("手动断开连接")); + LOGGER.info("[断开连接] 已断开服务器连接"); + } + + // 返回标题页面 + mc.setScreen(new TitleScreen()); + LOGGER.info("[断开连接] 已返回标题页面"); + } catch (Exception e) { + LOGGER.error("断开连接失败: {}", e.getMessage(), e); + } + }); + return true; + } catch (Exception e) { + LOGGER.error("断开连接异常: {}", e.getMessage(), e); + return false; + } + } + + /** + * 自动重连监听器 + */ + @Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) + public static class AutoReconnectListener { + private static boolean wasInGame = false; + + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + return; + } + + // 检测是否从游戏中断开 + boolean isInGame = mc.level != null && mc.getConnection() != null; + if (wasInGame && !isInGame && persistentConnection && lastServerAddress != null) { + // 刚刚断开连接 + lastDisconnectTime = System.currentTimeMillis(); + LOGGER.info("[自动重连] 检测到断开连接,将在1分钟后重连(超大型模组服需要更长时间加载)"); + } + wasInGame = isInGame; + + // 如果启用了持久连接且在标题屏幕 + if (persistentConnection && lastServerAddress != null) { + if (mc.screen instanceof TitleScreen && !isInGame) { + // 检查是否应该重连 + long currentTime = System.currentTimeMillis(); + if (currentTime - lastDisconnectTime >= RECONNECT_DELAY && lastDisconnectTime > 0) { + LOGGER.info("[自动重连] 正在重新连接到服务器: {}", lastServerAddress); + connectToServer(lastServerAddress, lastProxyAddress); // 使用上次的代理设置 + lastDisconnectTime = 0; // 重置,避免重复连接 + } + } + } + } + } + + /** + * 生成 HTML 页面 + */ + private static String generateHTML() { + return "\n" + + "\n" + + "\n" + + " \n" + + " Forge 游戏状态监控\n" + + " \n" + + "\n" + + "\n" + + "

Forge 游戏状态监控

\n" + + "
\n" + + "
当前状态
\n" + + "
加载中...
\n" + + "
\n" + + "
\n" + + "
自动刷新: 每 500ms
\n" + + "
\n" + + "
\n" + + "
服务器连接
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + " \n" + + " \n" + + "
\n" + + "
\n" + + "
\n" + + " \n" + + "\n" + + ""; + } + + /** + * 发送响应 + */ + private static void sendResponse(HttpExchange exchange, String response, String contentType) throws IOException { + byte[] bytes = response.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", contentType); + exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + + /** + * 发送错误响应 + */ + private static void sendError(HttpExchange exchange, int code, String message) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); + exchange.sendResponseHeaders(code, message.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(message.getBytes(StandardCharsets.UTF_8)); + } + } + + /** + * 转义 JSON 字符串 + */ + private static String escapeJson(String str) { + if (str == null) return ""; + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } +} + diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/TitleScreenDetector.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/TitleScreenDetector.java new file mode 100644 index 0000000..ce18278 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/TitleScreenDetector.java @@ -0,0 +1,55 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.slf4j.Logger; + +/** + * 标题屏幕检测器 + * 当游戏加载完毕进入标题屏幕时,在日志中输出提示信息 + * 用于帮助判断模组是否加载完毕(在模组较多的情况下加载很慢) + */ +@Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) +public class TitleScreenDetector { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static boolean hasLoggedTitleScreen = false; + + /** + * 监听客户端 Tick 事件 + * 在每个客户端 tick 中检查当前屏幕是否是标题屏幕 + * 当检测到标题屏幕时,输出日志提示 + */ + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + // 只在 tick 结束时检查(避免重复检查) + if (event.phase != TickEvent.Phase.END) { + return; + } + // 只在第一次检测到标题屏幕时输出日志 + if (!hasLoggedTitleScreen) { + Minecraft mc = Minecraft.getInstance(); + // 检查当前屏幕是否是标题屏幕 + if (mc != null && mc.screen instanceof TitleScreen) { + // 输出日志,提示用户已进入标题屏幕 + LOGGER.info("========================================"); + LOGGER.info("标题屏幕,使用鼠标或Tab键选择控件"); + LOGGER.info("========================================"); + hasLoggedTitleScreen = true; + } + } + } + + /** + * 重置标志(当退出到标题屏幕时,可以再次输出) + * 这会在游戏关闭或重新加载时被调用 + */ + public static void reset() { + hasLoggedTitleScreen = false; + } +} diff --git a/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/WorldTimeHUD.java b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/WorldTimeHUD.java new file mode 100644 index 0000000..a910ae4 --- /dev/null +++ b/autosaveforForge/src/main/java/com/xiaofan/autosaveforforge/WorldTimeHUD.java @@ -0,0 +1,78 @@ +package com.xiaofan.autosaveforforge; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.RenderGuiOverlayEvent; +import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.slf4j.Logger; + +/** + * 世界时间 HUD 显示 + * 在屏幕顶部中间显示服务器世界时间 + */ +@Mod.EventBusSubscriber(modid = Autosaveforforge.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) +public class WorldTimeHUD { + private static final Logger LOGGER = LogUtils.getLogger(); + + @SubscribeEvent + public static void onRenderGuiOverlay(RenderGuiOverlayEvent.Post event) { + // 在所有HUD元素渲染完成后渲染时间显示 + // 使用 CROSSHAIR 层作为触发点(这个层在游戏界面中总是渲染) + if (event.getOverlay() != VanillaGuiOverlay.CROSSHAIR.type()) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || mc.player == null) { + return; + } + + // 只在游戏界面显示,不在菜单界面显示 + if (mc.screen != null) { + return; + } + + // 获取服务器世界时间(不是客户端本地时间) + // getDayTime() 返回的是服务器同步的世界时间 + long worldTime = mc.level.getDayTime(); + + // 计算游戏内时间 + // Minecraft 一天 = 24000 刻 + // 游戏内时间从 0 刻(6:00)开始 + long dayTime = worldTime % 24000; + + // 计算小时和分钟 + // 游戏内时间:0刻 = 6:00, 1000刻 = 7:00, 6000刻 = 12:00, 18000刻 = 18:00 + int hours = (int) ((dayTime / 1000 + 6) % 24); + int minutes = (int) ((dayTime % 1000) * 60 / 1000); + + // 格式化时间显示:00:00,xx刻 + String timeString = String.format("%02d:%02d", hours, minutes); + String tickString = String.format("%d刻", dayTime); + String displayText = timeString + "," + tickString; + + // 获取屏幕尺寸 + int screenWidth = event.getWindow().getGuiScaledWidth(); + + // 计算文本宽度以居中显示 + int textWidth = mc.font.width(displayText); + int x = (screenWidth - textWidth) / 2; + int y = 10; // 距离顶部10像素 + + // 渲染文本 + GuiGraphics guiGraphics = event.getGuiGraphics(); + + // 绘制半透明背景 + int padding = 4; + int bgColor = 0x80000000; // 半透明黑色 (ARGB: 80 = 50% 透明度) + guiGraphics.fill(x - padding, y - padding, x + textWidth + padding, y + mc.font.lineHeight + padding, bgColor); + + // 绘制文本(白色) + guiGraphics.drawString(mc.font, displayText, x, y, 0xFFFFFF, false); + } +} + diff --git a/autosaveforForge/src/main/resources/META-INF/mods.toml b/autosaveforForge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..9d9f3d9 --- /dev/null +++ b/autosaveforForge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,63 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader="javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the forge version +loaderVersion="${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license="${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId="${mod_id}" #mandatory +# The version number of the mod +version="${mod_version}" #mandatory +# A display name for the mod +displayName="${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional +# A file name (in the root of the mod JAR) containing a logo for display +#logoFile="autosaveforforge.png" #optional +# A text field displayed in the mod UI +#credits="Thanks for this example mod goes to Java" #optional +# A text field displayed in the mod UI +authors="${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description='''${mod_description}''' +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional + # the modid of the dependency + modId="forge" #mandatory + # Does this dependency have to exist - if not, ordering below must be specified + mandatory=true #mandatory + # The version range of the dependency + versionRange="${forge_version_range}" #mandatory + # An ordering relationship for the dependency - BEFORE or AFTER required if the dependency is not mandatory + # BEFORE - This mod is loaded BEFORE the dependency + # AFTER - This mod is loaded AFTER the dependency + ordering="NONE" + # Side this dependency is applied on - BOTH, CLIENT, or SERVER + side="BOTH"# Here's another dependency +[[dependencies."${mod_id}"]] + modId="minecraft" + mandatory=true + # This version range declares a minimum of the current minecraft version up to but not including the next major version + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" diff --git a/autosaveforForge/src/main/resources/autosaveforforge.mixins.json b/autosaveforForge/src/main/resources/autosaveforforge.mixins.json new file mode 100644 index 0000000..4ddc941 --- /dev/null +++ b/autosaveforForge/src/main/resources/autosaveforforge.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.xiaofan.autosaveforforge.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "autosaveforforge.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + }, + "overwrites": { + "requireAnnotations": true + } +} diff --git a/autosaveforForge/src/main/resources/pack.mcmeta b/autosaveforForge/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..21b7215 --- /dev/null +++ b/autosaveforForge/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "autosaveforforge resources", + "pack_format": 15 + } +} From 68b613961d53cb90e1b146442ed1eb69c29099b4 Mon Sep 17 00:00:00 2001 From: xiaofanforfabric <1706079731@qq.com> Date: Mon, 1 Dec 2025 17:03:40 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=E9=8F=87=E5=AD=98=E6=9F=8A?= =?UTF-8?q?=E7=BB=80=E8=BD=B0=E7=B7=A5=E7=91=99=E5=97=9B=EE=95=B6=E9=96=BE?= =?UTF-8?q?=E7=82=AC=E5=B8=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da2cb0d..c007888 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ autosave/ ##### 🎬 示例视频 -> 📹 **演示视频**:[点击观看自动化宏系统演示](https://example.com/video) *(请替换为实际视频链接)* +> 📹 **演示视频**:[点击观看自动化宏系统演示](https://web.xiaofansmp.cc/video/VID_20251201_021329.mp4) ##### 💡 示例代码 From f6621eca273eb65ce789a22819badcb4cc80d361 Mon Sep 17 00:00:00 2001 From: xiaofanforfabric <1706079731@qq.com> Date: Mon, 1 Dec 2025 17:04:21 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20=E7=81=8F=E5=97=99=E3=81=9A?= =?UTF-8?q?=E6=B8=9A=E5=AC=AD=EE=9D=8B=E6=A3=B0=E6=88=A6=E6=91=BC=E9=8E=BA?= =?UTF-8?q?=E3=83=A6=E6=95=BC=E6=B6=93=E7=AF=90TML=E7=91=99=E5=97=9B?= =?UTF-8?q?=EE=95=B6=E9=8D=8F=E5=86=AA=E7=A4=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c007888..a582a60 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,10 @@ autosave/ ##### 🎬 示例视频 -> 📹 **演示视频**:[点击观看自动化宏系统演示](https://web.xiaofansmp.cc/video/VID_20251201_021329.mp4) + ##### 💡 示例代码 From c37666dd6bcda6bf89525d60f062bd67001bea81 Mon Sep 17 00:00:00 2001 From: xiaofanforandroid <1706079731@qq.com> Date: Mon, 1 Dec 2025 17:08:33 +0800 Subject: [PATCH 4/4] Update README.md --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index a582a60..7228fb7 100644 --- a/README.md +++ b/README.md @@ -145,12 +145,7 @@ autosave/ 完整的宏语言语法和使用说明请查看:[**FanMacrodoc.md**](autosaveforForge/FanMacrodoc.md) -##### 🎬 示例视频 - - +##### 🎬 示例视频https://web.xiaofansmp.cc/video/VID_20251201_021329.mp4 ##### 💡 示例代码