下载
第三方下载与发布渠道:
| 渠道 | 说明 |
|---|---|
| Modrinth | 下载 Modrinth 版 |
| CurseForge | 下载 CurseForge 版 |
| Releases | 本仓库发版,所有 jar 挂在同一 Release |
| Issues | 反馈问题 |
拍照、翻相册、换壁纸、听歌、聊天、装 App。
第三方下载与发布渠道:
| 渠道 | 说明 |
|---|---|
| Modrinth | 下载 Modrinth 版 |
| CurseForge | 下载 CurseForge 版 |
| Releases | 本仓库发版,所有 jar 挂在同一 Release |
| Issues | 反馈问题 |
第三方基于 MCphone 的 App SPI 做的模组收在附属一览,其中也写了收录规则。它们各自独立发布与维护,不是 MCphone 的一部分。
当前发布的构建仅有 Minecraft 1.21.1 · NeoForge 一项,本 wiki 的截图与操作说明均以其为准。其余 Minecraft 版本与加载器(Forge、Fabric)尚无可供下载的构建。
仓库不按 Minecraft 版本或加载器分支,全部目标位于 main。发版时逐个目标构建,所有 jar 挂载于同一个 Release,文件名形如 mcphone-<版本>-<目标>.jar。
计划支持的目标及各自状态见 versions/targets.json。从源码构建见 从源码构建;本模组以 MIT 许可发布。
把一部能用的智能手机塞进 Minecraft —— 拍照、翻相册、换壁纸、听歌、装 App,还能给自己那一部手机起个名。
| 我想…… | 去哪 |
|---|---|
| 手机怎么做出来 | 快速开始 → 怎么拿到手机 |
| 换壁纸 / 改字色 | 设置 → 更换壁纸 |
| 手机太小看不清 | 设置 → 界面大小 |
| 给某个 App 绑一个键 | 设置 → 每个 App 一个快捷键 |
| 放自己的音乐 | 音乐 → 支持的格式 |
| MP3 放不出来 | 音乐 → MP3 只认 MPEG-1 |
| 聊天发图太占硬盘 | 服主须知 → 图片占多少硬盘 |
| 备份要备哪些目录 | 服主须知 → 数据存在哪儿 |
| 用资源包换手机贴图 | 换肤(资源包) |
| 商店里少了某个 App | 应用商店 → 联动 App 页 |
装什么、怎么合成、怎么开机、主屏怎么摆。
MCphone 自身没有任何前置。缺了谁都照常开机,只是少那一个 App。
| 模组 | 装了会怎样 |
|---|---|
| Curios | 手机多一个饰品栏槽位,可以挂在腰上 |
| Waystones | 多一个「传送石」App |
| MCEF | 多一个「浏览器」App |
| NetMusic | 它刻录的 CD 能放进唱片仓,走到哪儿放到哪儿 |
| Patchouli | 多一个「阅读」App,整合包里所有教程手册收进一个书城 |
| GuideME | 书城里多出用它做手册的模组那几本(AE2、现代化工艺都硬依赖它),自动发现,不用逐个适配 |
| Immersive Engineering | 书城里多一本《工程师手册》——那本书自成一套,靠白名单单独适配 |
| FTB Quests | 多一个「任务书」App |
| AE2 · Refined Storage · Tom's Simple Storage | 多一个「终端」App。三家有一家就够 |
| Integrated Dynamics | 不多任何功能,只让崩溃报告指认对人 |
未安装对应模组时,依赖它的 App 不出现在主屏和应用商店的普通列表中——商店里放一个点了必然报错的东西,比它不存在更糟。缺了什么则由商店的「联动App」页回答。
工作台合成,一次一部:
玻璃 玻璃 玻璃
红染料 黄染料 蓝染料
铁锭 红石 铁锭| 操作 | 结果 |
|---|---|
| 手持右键 | 开机 |
| 按 H | 手机在背包或饰品槽里也能开机,不必先切到手上 |
| 按 Esc / 点机身外面 | 关机 |
| 屏幕底部导航栏 ◁ | 退回上一层 |
Esc 不分层级:开到哪一页按下去都是直接关机;要退一层走 ◁。
模组注册的五个键,均可在原版「选项 → 按键设置 → MCphone」中改:
| 键 | 默认 | 作用 |
|---|---|---|
| key.mcphone.open_phone | H | 开机 |
| key.mcphone.camera_shutter | V | 相机:拍照 |
| key.mcphone.camera_exit | X | 相机:退出 |
| key.mcphone.hud_toggle | G | 唤出 / 收起副手 HUD |
| key.mcphone.hud_interact | 左 Alt | 唤出 / 收起 HUD 上那部手机的鼠标操作 |
五个默认键在 Minecraft 1.21.1 原版中均未被占用。此外每个 App 还能各绑一个自己的快捷键,见每个 App 一个快捷键。
按住图标直接拖动即可换位,其余图标实时让位,松手落定。
| 项 | 值 |
|---|---|
| 起拖阈值 | 移动 3 像素 才算拖动,轻点仍是打开 App |
| 移动方式 | 插入,不是对调——把第一个拖到第三格,中间那些依次前移 |
| 顺序存放 | 客户端 config/mcphone/installed/<存档>.json,按存档记 |
| 新装的 App | 落在最后一格,不打乱已有排列 |
| 项 | 值 |
|---|---|
| 每行 | 4 个 |
| 每页行数 | 最多 5 行(上限;实际行数按屏幕剩余高度计算) |
| 页码点 | 直径 3 像素,间距 4;只有一页时不绘制 |
只有一页时画一个孤零零的点,会让人以为还能往旁边划。翻页三种方式:
| 方式 | 参数 |
|---|---|
| 滚轮 | — |
| 在空白处横向滑动 | 位移超过 24 像素 触发 |
| 点击页码点 | 直接跳转 |
翻页动画 160 毫秒。
拖着图标停在屏幕左右边缘:边条(宽 10 像素)由浅到深亮起,停满 0.4 秒 自动翻页,手不必松开。
不是「碰到就翻」:拖向最右一列的路上必然扫过右边条,一碰就翻的话最后一格永远放不进去。
手机拿在副手里时自动挂到画面上,一直亮着,停在哪一页就显示哪一页——边走边看聊天、边挖矿边看任务书,不必反复开机。
| 情形 | 行为 |
|---|---|
| 手机在副手 | 自动挂上 |
| 手机在 Curios 饰品栏、背包或主手 | 按 G 手动叫出,再按一次收起 |
| 按下 F1(原版隐藏 HUD) | 跟着一起隐藏 |
G 顶掉自动那条规矩,而不是与之并列:手机在副手上时按 G 也能收起,否则会出现「按了没反应」。副手上的手机被拿走或换掉后,自动那条重新生效。
按 Alt 唤出鼠标,再按一次收起 ——是切换,不是按住。唤出后可以翻页、点 App、在输入框里打字。
| 情形 | 行为 |
|---|---|
| 背包或聊天框正开着时按 Alt | 不抢。玩家正在那儿操作 |
| 手机全屏打开时按 Alt | 无效果。此时已经在正经操作手机 |
| 操作中点了机身外、或被别的界面顶掉 | 退出操作态,手机退回 HUD 继续挂着 |
| 从全屏退出(ESC、点机身外) | 自动挪回 HUD 那个角上 |
挂着的和 Alt 里的是同一部手机,不是两块屏:在 Alt 里翻到第三页,收起之后 HUD 上还是第三页。唤出鼠标期间是原版界面状态,人不会走动。
「设置 → 副手 HUD」里可开关、调大小与位置;也可以就地摆——拖边框或状态栏挪位置,滚轮缩放。
| 项 | 范围 | 默认 |
|---|---|---|
| 大小 | 40%–150% | 60% |
| 锚点 | 九个角(上/中/下 × 左/中/右) | 右下 |
| 偏移 | ±4096 像素,横竖各一 | 0 |
位置按锚点 + 偏移记录,换分辨率、改 GUI 缩放之后仍停在原处。
HUD 的大小与「设置 → 界面大小」是两个独立的数:一个管挂在画面上那部,一个管全屏打开那部。
| 键 | 默认 | 作用 |
|---|---|---|
| key.mcphone.hud_interact | 左 Alt | 唤出 / 收起鼠标操作 |
| key.mcphone.hud_toggle | G | 手动挂出 / 收起 HUD |
均可在原版「选项 → 按键设置 → MCphone」中改键。
config/mcphone-client.toml:
| 键 | 默认 | 说明 |
|---|---|---|
| hudEnabled | true | 总开关 |
| hudAnchor | BOTTOM_RIGHT | 贴哪个角,九选一 |
| hudOffsetX / hudOffsetY | 0 / 0 | 从锚点再往里挪多少像素 |
| hudScale | 60 | 百分比,40–150 |
手机里预置的十五个 App,各自去哪一页看。
| App | 说明 |
|---|---|
| 相机 | 取景框 + 分帧截图,照片存进 screenshots/ |
| 相册 | 缩略图网格、大图查看、删除 |
| 音乐 | 放你自己的音乐;唱片仓能外放给周围人听 |
| 时钟 | 游戏内时间与还有多久天黑,下界也准 |
| 天气 | 现在什么天,以及这种天适合做什么 |
| 应用商店 | 安装 / 卸载 App |
| 设置 | 壁纸、字体颜色、设备命名、App 管理器 |
| 美西螈 | 手机上的聊天 App:好友、会话、发图与表情、传送到好友身边 |
| 记事本 | 随手记,能印成书 |
| 末影箱 | 随身开末影箱。付费,1 × 末影箱 |
| 传送石 | 去任何已激活的传送点。需 Waystones,付费,1 × 传送石 |
| 浏览器 | 在手机里上网。需 MCEF |
| 阅读 | 整合包里的教程书全在这儿。联动 Patchouli 等 |
| 任务书 | 开机点一下就是任务书。联动 FTB Quests |
| 终端 | 打开你自己的存储终端。联动 AE2 / RS / Tom's,免费 |
相机负责取景与截图,相册负责浏览与删除。两者共用游戏目录下的 screenshots/。
取景框 + 分帧截图。照片存进游戏目录的 screenshots/,与原版 F2 截的图放在一起。
| 键 | 默认 | 作用 |
|---|---|---|
| key.mcphone.camera_shutter | V | 拍照 |
| key.mcphone.camera_exit | X | 退出相机 |
均可在原版「选项 → 按键设置 → MCphone」中改键。
在「设置 → App 管理器 → 相机」中,「快门闪光」一行点击即在两者间切换。
| 模式 | 表现 | 配置值 |
|---|---|---|
| 白闪(默认) | 整个屏幕瞬间提到全白 | cameraSoftFlash = false |
| 模糊 | 画面糊一下再收回,亮度不变 | cameraSoftFlash = true |
闪光持续 220 毫秒。两种都不会进照片 ——闪光在抓图完成之后才开始。
模糊借用原版菜单背景那条后处理链,不额外附带着色器文件;半径随时间收敛,因此是化开又收回,而非硬切一块糊画面。
白闪是照相机的惯例,但在夜间或暗处拍照时会把屏幕顶到全白,眼睛需要缓几秒。模糊同样交代了「拍下来了」,而不改变亮度。
设置项为客户端级,存于 config/mcphone-client.toml。
| 项 | 值 |
|---|---|
| 目录 | 游戏目录下的 screenshots/ |
| 网格 | 每行 3 张,分页浏览 |
| 缩略图 | 长边 96 像素,按需生成并缓存 |
| 大图预览 | 长边 512 像素 |
| 翻页 | 底部 ◁ ▷ |
| 大图内切换 | 左右方向 |
| 删除 | 二次确认:第一次点击后按钮变为「再点一次确认」 |
右上角「打开文件夹」用系统文件管理器直接打开截图目录。
相册也是美西螈发图时的图片来源;聊天里收到的图可以存进相册,存的是原始文件。
播放 config/mcphone/music/ 下玩家自己的音乐文件,另有唱片仓可对周围人外放。播放 / 暂停 / 继续、上一首下一首、三种循环模式、进度条、音量。
| 曲库(耳机) | 唱片仓(外放) | |
|---|---|---|
| 内容来源 | 仅 config/mcphone/music/ 下的文件,游戏内唱片不混入 | 手持唱片点击放入,仓位 1 格 |
| 谁听得见 | 只有本人 | 周围玩家都听得见,声音跟随移动 |
| 由谁解码 | 本地客户端 | 服务端播放原版音效 |
| 可用操作 | 播放 / 暂停 / 继续 / 切换 / 进度 / 音量 | 仅播放与停止 |
外放没有暂停:原版音效系统中不存在「从中间接着放」。两条路可同时发声。
装了 NetMusic 后,唱片仓同时接收它刻录的 CD:用其电脑方块搜歌、刻成 CD,放入手机后按播放,走到哪儿放到哪儿。
网络音乐能外放而本地文件不能,原因在于服务端只需广播一个地址,每个客户端各自去拉取。搜歌、登录、会员、刻盘均属 NetMusic;本模组只接手「播放」这一件。
放入 config/mcphone/music/ 即可,进入音乐 App 会自动重扫,无须重启游戏。
| 解码器 | 扩展名 | 说明 |
|---|---|---|
| 原版 Vorbis | .ogg .oga | 游戏本体自带,最稳 |
| JavaMP3 | .mp3 .mp2 .mp1 | MIT 许可,全文见 jar 内的 THIRD-PARTY.txt |
| 内置 PCM | .wav .wave .aiff .aif .au | — |
| 采样率 | 是否可播 | |
|---|---|---|
| MPEG-1 | 44100 / 48000 / 32000 Hz | 是(绝大多数音乐属于此档) |
| MPEG-2 / MPEG-2.5 | 22050 / 24000 / 16000 / 11025 / 12000 / 8000 Hz | 否 |
这是所打包解码库的限制,对应其 issue #8。
不可播放的文件不会静默失败:曲库中该行置灰,悬停提示「MPEG-2 放不了,只认 MPEG-1」,日志中记录完整规格:
[MCphone] 打开 MP3 我的歌.mp3 —— MPEG-1 Layer III, 44100Hz, 立体声, 320kbps
[MCphone] 放不了 local:老歌.mp3:MPEG-2 Layer III, 22050Hz, 单声道, 64kbps。
这个播放器只认 MPEG-1(44100 / 48000 / 32000 Hz),请转成 44.1kHz 再放进来用任意转码工具转成 44.1kHz 的 MP3 即可,或转成 OGG。
循环模式与音量存于 config/mcphone-client.toml:
| 键 | 默认 |
|---|---|
| musicMode | LIST_LOOP | (列表循环 / 单曲循环 / 随机)
| musicVolume | 100 | (范围 0–100)
音量为该 App 自身的音量,最终输出还要乘以游戏主音量与唱片音量。
两个预装的小 App:游戏里几点了,现在什么天。
显示游戏内时间与现实时间。
原版时钟只有一个转盘,看得出大概是白天还是晚上,看不出「几点」,也看不出「还有多久天黑」——而后者才是玩家真正在问的问题。
| 常量 | 值 |
|---|---|
| 一整天 | 24000 tick |
| tick 0 对应 | 早上 6:00(偏移 6 小时) |
| 每现实秒 | 20 tick |
| 日落开始 | tick 12000 |
| 入夜 | tick 13000 |
| 日出开始 | tick 23000 |
换算逻辑集中在 feature/clock/WorldClock,该类不接触 Minecraft,可单独跑断言。时间算错既不会崩溃也不会报错,只会让所有人的时钟差 6 小时。这类错误必须在进游戏之前就拦住。
显示当前天气,以及这种天适合做什么。
「现在下不下雨」抬头就看得见;有意义的是后半句:下雨适合钓鱼(雨天上钩快),打雷是抓高压苦力怕的时候,下雪可以拿铲子收雪堆雪傀儡。这些都是原版真实存在的机制。
| 判定结果 | 含义 | 图标 |
|---|---|---|
| CLEAR | 晴 | weather/clear.png |
| RAIN | 下雨 | weather/rain.png |
| SNOW | 下雪 | weather/snow.png |
| THUNDER | 雷雨 | weather/thunder.png |
| DRY | 别处在下雨,这里不下(沙漠、恶地),天是阴的 | weather/dry.png |
| NONE | 这个维度没有天气(下界、末地) | weather/none.png |
六张图标均为 32×32,位于 assets/mcphone/textures/weather/,见换肤。
判定逻辑集中在 feature/weather/Weather,同样不接触 Minecraft,可单独跑断言。沙漠、雪山、下界这几种情况在自己的存档里根本试不出来,而判错了不会崩溃——手机只会在沙漠里说「正在下雨」。
手机上的聊天 App,名字就叫这个。双向好友、会话列表(未读数、在线状态、最后一条预览)、气泡式会话界面。
| 项 | 值 |
|---|---|
| 好友 | 100 人 |
| 每人待处理申请 | 50 条 |
| 每对会话保留消息 | 最近 100 条 |
| 单条消息长度 | 256 字 |
| 每对会话保留图片 | 最近 20 张(按内容去重后计) |
| 单张图片体积 | 由服主定,默认 512 KB,见服务端配置 |
| 图片长边 | 512 像素 |
| 动图帧数 | 6–36 帧 |
加好友需对方同意。在右上角的「+」中从当前在线玩家里点选,对方在同一界面同意或拒绝;两人同时点「添加」直接成为好友。
| 规则 | 说明 |
|---|---|
| 聊天范围 | 仅限好友之间,陌生人发不进来 |
| 解除好友 | 聊天记录不删除,重新加回后历史即恢复 |
| 在线状态刷新 | 每 3 秒一次 |
输入栏左侧的「+」中有两项:图片与表情。
即相册内容:相机 App 拍的、按 F2 截的都在其中,选中点击即发送。
存放于 config/mcphone/stickers/,跟随客户端,换服务器、换存档均保留。导入方式两种:
发送前客户端先行压缩:
| 步骤 | 规则 |
|---|---|
| 1 | 长边压到 512 像素 |
| 2 | 仍超出体积上限时,依次降档重压:384 → 320 → 256 → 192 |
| 3 | 仍压不下则提示更换图片 |
雨天、树叶一类满屏噪点的截图 PNG 压不动,是走到第 2 步的主要情形。
| 其他限制 | 值 |
|---|---|
| 上传超时 | 15 秒 |
| 发送冷却 | 2 秒 |
| 同时排队 | 最多 4 张 |
| 分块传输 | 每块 16 KB |
不存原图:手机放大看图时也只有 448×680 个真实像素,存原图只是占用服主硬盘与所有人的带宽。
GIF 表情发出后在对方手机上是动的:拆成帧、拼成一张雪碧图传输,播放在客户端完成。
| 约束 | 值 |
|---|---|
| 帧数 | 6–36 帧 |
| 雪碧图长边 | 最大 1024 像素 |
| 超出上限时 | 先降一档帧尺寸;再不行隔帧抽稀;仍塞不下则退回发送第一帧 |
表情页格子中显示的是第一帧——该页不做动画。
收到的图片直接显示,不套气泡。
| 操作 | 结果 |
|---|---|
| 点击图片 | 放大铺满屏幕 |
| 放大后点右上角 | 存进相册,存的是原始文件,存完即可再转发 |
| 点击别处 | 收起 |
动图存进相册的是第一帧 ——相册是给截图用的,动图放进去别处都读不了。
保留该行而不删除:整条删去会使聊天记录凭空缺失若干句,且缺失的内容无从追溯。文字消息不受此限制,仍为每对会话 100 条。
服主可整个关闭图片功能,见服务端配置。
在线好友那一行的右下角有一个 7×7 的图标,点击即传送,手机同时自动关闭。
| 项 | 行为 |
|---|---|
| 落点 | 与原版 /tp 玩家 玩家 一致:对方所站坐标,朝向也一致 |
| 跨维度 | 支持 |
| 是否需对方同意 | 否。好友本身已是双方同意的结果 |
| 对方是否知情 | 是。两端各响一次末影人传送声,其动作栏写明是谁来了 |
| 费用与冷却 | 无 |
| 离线好友 | 不显示该图标 |
鼠标悬停时图标点亮,该行第二行的消息预览临时替换为「传送到他身边」。
对方恰在点击瞬间下线时提示「对方已经不在线了」——在线状态 3 秒刷新一次,这几秒的空档撞得上。图标放在右下角,是为了不与未读数、时间挤占位置,名字也不必让出宽度。
三个各自一句话说得完的 App。
| App | 价格 | 前置 |
|---|---|---|
| 记事本 | 免费 | 无 |
| 末影箱 | 1 × 末影箱 | 无 |
| 传送石 | 1 × 传送石(waystones:warp_stone) | Waystones |
价格由内建的报价来源 feature/store/BuiltinAppPrices 声明,机制见IAppPriceProvider。
随手记录文字。
| 项 | 说明 |
|---|---|
| 存放 | 玩家数据(playerdata/),死亡不掉落 |
| 输出 | 写完可印成一本书交给别人 |
随身打开自己的末影箱。就是原版那一个,与方块末影箱、跨维度完全互通。
打开传送石碑的选点界面,前往任意一个已激活的传送点。
该价格仅在注册表中确实存在该物品时才登记——Waystones 缺失时这个 App 本身就不会出现。
在手机里上网。需要 MCEF。
它是唯一一个跳出机身的 App:手机屏幕为 120×200 像素,网页在其中一行正文放不下十个字,因此点开后是一块占屏幕九成的居中面板,退出即回到手机。
面板上方一条高 22 像素的工具条:后退、前进、刷新、地址栏,右侧圆点为加载指示。工具条位于面板之外,浮在面板上方的留白里,不占用网页高度。
输入内容按以下顺序判定:
| 顺序 | 条件 | 行为 |
|---|---|---|
| 1 | 含 :// | 原样打开 |
| 2 | 含空格 | 作为搜索词 |
| 3 | 含 . | 补 https:// 后打开 |
| 4 | 其余 | 作为搜索词 |
MCEF 自带的快捷键可用(macOS 上以 ⌘ 代替 Ctrl):
| 键 | 作用 |
|---|---|
| Ctrl + 滚轮,或 Ctrl + = / - / 0 | 缩放 |
| Alt + ← / → | 后退 / 前进 |
| Ctrl + R | 刷新 |
成因在 MCEF:它没有注册 CefLifeSpanHandler,而离屏渲染下也没有窗口可开,于是点击不产生任何效果。变通办法:把地址复制进地址栏手动打开。
浏览器后端本身是一个可替换的扩展点,开发者侧见浏览器后端。
把整合包里的教程手册全列在一页里。联动 Patchouli、GuideME、沉浸工程。
整合包里的教程书通常有几十本,每一本都是一个物品:想查点什么就得先在仓库里翻出那本书,翻完还得放回去;出门在外想起要看,书多半不在身上。列表中一行是书名,第二行是它出自哪个模组(玩家找书的思路几乎总是「某某模组那本书」,而不是书名)。行首小图即该书自身的物品图标。
底部两个页签:
| 页签 | 内容 |
|---|---|
| 书城 | 整合包里有的全部书,几十本 |
| 书架 | 玩家自己收下的那几本。打开 App 默认停在这里 |
收藏靠每行右端的 ☆:点一下进书架,再点一下移出。不用长按、也不另开菜单——长按在这种小屏上没有任何提示,等于把功能藏起来。
| 项 | 说明 |
|---|---|
| 收藏存放 | config/mcphone/reader/shelf.json |
| 作用域 | 全局一份,不分存档 |
| 换整合包后 | 当前认不出的书不显示,但不从文件中删除;换回去仍在架上 |
| 停留页签 | 一局之内记住——从书城点开一本书,看完回来仍在书城 |
按存档分的结果会是「同一个包、换个服务器书架就空了」:书来自客户端装的模组,同一个整合包连到哪个服务器都是那几本。
两个页签顶部都是搜索框,进入该页时它已持有焦点,直接打字即可。可搜三个字段:书名、模组名、模组 id。中文客户端里书名是中文、出处是英文很常见;只搜一个字段就会出现「我明明记得这个模组叫什么,却搜不到它的书」。
结果按分数从高到低排列:
| 命中方式 | 分数 |
|---|---|
| 书名完全相同 | 100 |
| 书名前缀 | 80 |
| 书名包含 | 60 |
| 模组名前缀 | 45 |
| 模组名包含 | 35 |
| 模组 id 包含 | 20 |
用空格分开,是与的关系,各词可以命中不同字段(例:ars 笔记)。任一词未命中,整本即不算命中;整本的分数取最弱那一项的分。右上角的数字在搜索时变为「命中 / 总数」。
这一页吃掉所有按键(含背包键),与记事本、设备命名几页相同:打拼音必然按到 e,不吃掉就成了开背包。ESC 照旧直接关机。尚未实现拼音搜索(xsmy → 新生魔艺),那需要一张拼音表。
点开一本书后,接管屏幕的是它自己的界面,进度、已读标记、条目锁定全部对得上,与在背包里右键该书一致。
本模组只提供目录:Patchouli 的书界面是 272×180,手机屏幕只有 120×200,放不下;而自行重画就要把它几十种页面类型重新实现一遍,它一改版即断。
| 来路 | 收录方式 | 典型例子 |
|---|---|---|
| Patchouli | 扫描 | 绝大多数教程书 |
| GuideME | 自动发现 | AE2、现代化工艺 |
| 白名单 | 写死一份名单 | 沉浸工程《工程师手册》 |
AE2 的作者把自家手册系统拆成了独立模组 GuideME,AE2 与现代化工艺都硬依赖它。GuideME 自身即具备「列出全部手册」的能力,因此这一支是自动发现——不写死任何一本书的 id,将来第三、第四个模组接上它,书城里自动就有。
点开走的是它手册物品右键那条路,因此在手机里点开与在背包里右键进入的是同一个界面、同一段历史。图标是它为每本手册创建的那个物品。
有的模组自写了一整套手册系统,与 Patchouli 无关,扫描扫不出来——沉浸工程的《工程师手册》是第一个。这类书走一份写死的白名单,每条记录它出自哪个模组、用哪个物品作图标与书名、以及如何打开。打开走的是它自己物品右键那条路(ManualHelper.getManual().getGui()),连「停在上次看的那一页」都一致。书名取自该物品的名称,与背包、JEI 中显示的是同一行字。
新增一个模组:写一个 ExternalBook 实现,向 ExternalBookSource 的白名单加一行。对方未安装时自动跳过。
有些模组的书是 no_book 的,游戏内没有对应物品,只能用命令打开。它们在这里同样列出并可打开,图标使用默认的书。
有些模组把自己的手册整个换掉了,Patchouli 中只剩一本无人维护的旧书。这类由「书籍特例」层处理,它能做三件事:把某本书从架上移除、改写其显示、接管「点开之后打开什么」。
新增一条:写一个 BookQuirk 实现,加入 BookQuirks 的名单。对方未安装时自动跳过。
这类代码一律走反射、不加编译依赖:为一个方法引入几十 MB 的前置不值,且断了要能退回默认行为(打开那本遗留的 Patchouli 书),不能变成一个点了没反应的死按钮。
开机点一下就是整合包的任务书。联动 FTB Quests,预装且免费。
任务书是一局里翻得最勤的东西——每做完一件事就要回去看下一步。而它是个物品:要么长期占着快捷栏一格,要么每次都得回箱子里翻出来。这一格把它变成「开机 → 点一下」,书仍在原处,只是不必随身携带。
FTB Quests 缺失时,本 App 不出现在主屏与商店的普通列表中,但商店的「联动App」页照常列出它并写明缺哪个模组。
点开后接管屏幕的是它自己的界面,章节、进度、领奖、编辑权限全部对得上,与在背包里右键那本书一致——所调用的正是那本书右键时执行的那一句。
加了的话玩家点下去只会得到静默无反应;交给它,玩家看到的和右键实体书时是同一句话。
任务档案本身即同步在客户端,开界面也在客户端,服务端不会多收一个包。
接触面只有一个零参方法,走反射、不加编译依赖,理由与阅读的几支书源相同,写在 feature/quests/client/FtbQuestsBook.java 的类注释里。
任务书在绝大多数整合包里是开局白送、丢了还能再做的,卖它没有对应物;而这个 App 存在的意义就是少走几步,埋进商店等玩家自己发现,等于第一步就多走了。
| 教程书 | 任务书 | |
|---|---|---|
| 数量 | 几十本 | 全局一本 |
| 使用方式 | 想查点什么才去翻 | 随时要看 |
| 因此需要 | 一个带搜索的列表 | 一个直达的入口 |
收进书城的话每次都要走「开机 → 阅读 → 从几十本里找到它 → 点开」,比拿实体书还慢——多那一层列表,这个 App 就没有存在的必要了。
在手机上点一下,打开你自己的存储终端。装了哪家存储模组,开出来的就是哪家的界面。免费。三家有一家就够。
| 模组 | modid | 认得的东西 |
|---|---|---|
| Applied Energistics 2 | appliedenergistics2 | 无线终端、无线合成终端、无线样板访问终端 |
| └ 加装 AE2WTLib | ae2wtlib | 它那些终端也全认,包括通用终端 |
| Refined Storage 2 | refinedstorage | 无线网格、无线自动合成监视器、便携网格 |
| Tom's Simple Storage | toms_storage | 高级无线终端(基础那把开不了,见下) |
装了哪几家,App 详情页的简介末尾会写出来。
手机内置一个终端卡槽。点开「终端」时:
| 情形 | 行为 |
|---|---|
| 卡槽里装了终端 | 直接开那一台 |
| 卡槽空着 | 打开卡槽界面,格子与背包相邻,拖入即装好 |
| 按住 Shift 点图标 | 一律打开卡槽(用于换一台或取出) |
它解决的是「身上带着好几台,到底开哪一台」:答案从「背包顺序里第一台」变成玩家自己选的那一台。
卡槽里的终端死亡不掉落,与壁纸、笔记、唱片仓中的唱片一样跟随玩家。
这一条是刻意的:终端一旦收进手机就不在背包里,各家自己的快捷键(AE2WTLib 的补货/磁铁/收纳、RS 的打开无线网格)便找不到它。想用那些快捷键的人不装卡槽即可,代价只是多按一下。
因此它放不进卡槽;背包里带着它点「终端」会跳过它并说明原因——比点了没反应要好。要隔空打开,用高级那把。
| 因此 | |
|---|---|
| 进度 | 你在 AE2 / RS / Tom's 里的进度即为你在这一格里的进度——增幅卡、绑定网络、跨维度全按对方规则 |
| 打不开时 | 说话的也是它们(如 AE2 的 Device is not linked.),本模组不在前面加自己的检查 |
加了自己的检查,两套规则迟早对不上,而对不上的表现是「手机上说不行、手里拿着又能开」。
末影箱那一格卖一个末影箱、传送石那一格卖一个传送石,因为 App 本身就是那件实物的替代品。这一格替代不了任何东西——它开的是你自己那台终端,没有终端它一格内容都没有。真正的门槛是那台终端,再收一次等于收两遍。
安装与卸载 App。安装状态持久化,系统 App 置灰且不可卸载。
| 项 | 说明 |
|---|---|
| 安装状态存放 | 客户端 config/mcphone/installed/<存档>.json,与主屏排列同一份 |
| 系统 App | 卸载键置灰并写明原因,例如「设置」 |
| 付费 App | 详情页显示价格,买不起时按钮置灰。见小工具 |
| 卸载后 | 仍可从商店重新安装 |
App 也可以由附属模组自带的来源提供,见IAppSource。
商店最后一格是「联动App」,排在所有可下载 App 之后。点进去一行一个列出:图标、名字、缺哪个模组、已装还是未装。未满足的整行压暗,看得见、点不动。
它回答的是「我怎么没有这个 App」——依赖缺失的 App 不出现在主屏与商店普通列表中,否则玩家会点到一个必然报错的图标;但完全不出现,玩家也就无从知道自己缺了什么。
| 声明方式 | 是否收录 |
|---|---|
| requiredMods() | 是 |
| companionMods() | 是(该 App 当前不可用时) |
| 当前可用的 App | 否——它没有被任何模组卡住,列出只是噪声 |
手机内「设置 → 关于」的联动模组列表则一律列出这些模组,并标明装没装。开发者侧的声明方式见 companionMods()。
App 管理器、快捷键、壁纸、界面大小、字体颜色、设备名称、副手 HUD。
| 这一页 | 干什么 | 存在哪 |
|---|---|---|
| App 管理器 | 每个 App 一页:来历、前置/联动、卸载、快捷键 | — |
| 每个 App 一个快捷键 | 按一个键直接开机进某个 App | mcphone-client.toml |
| 更换壁纸 | 选一张 PNG 当壁纸 | 服务端,按玩家 |
| 界面大小 | 把整个手机放大缩小 | mcphone-client.toml |
| 字体颜色 | 六个预设 | mcphone-client.toml |
| 设备名称 | 给自己这部手机起名 | 服务端 |
| 副手 HUD | 手机挂在画面上:开关、大小、位置 | mcphone-client.toml |
| 关于 | 版本、联动模组装没装 | — |
列出已安装的每一个 App。点一行进入它自己的一页:图标、名字、作者、版本、介绍,以及它由哪个模组提供、所声明的前置与联动模组装没装。
卸载在这一页,且需点击两次(第一次上膛、第二次才真卸),与相册删照片同一条规矩。系统 App 的卸载键置灰并写明原因。卸载后仍可从应用商店装回。
这一页往后是每个 App 的开关都该待的地方——操作区是一行一个的形状,加一个开关就是加一行。第一个这样的开关是快捷键,第二个是相机的快门闪光。
在「设置 → App 管理器 → 某个 App」中点击「快捷键」一行,然后按一个键。此后在世界中按下该键即直接开机并进入该 App。默认未指定。每个 App 各绑各的,已绑定的键显示在管理器的列表行上。
| 键 | 含义 |
|---|---|
| 任意键盘键 | 绑定 |
| 鼠标侧键(4/5)、中键、右键 | 绑定。与原版「按键设置」可绑范围一致 |
| 鼠标左键 | 取消本次绑定 |
| ESC | 清除已有绑定(与原版「按键设置」含义一致) |
| 再次点击该行 | 取消本次绑定 |
左键是玩家在手机里点东西的那只手,等键时点一下即为取消;真绑上去的话,在世界中每挖一次方块都会开一次手机。绑在鼠标键上的 App 打开时不会顺带挥手或使用一次物品 ——那一下会被整个吃掉。
按住 Ctrl / Shift / Alt 再按主键即为组合,可叠加(Ctrl+Shift+K),鼠标键同样可以(Ctrl+侧键 4)。修饰键本身按下不算数,须按下主键那一刻才成立。macOS 上的 Command 即此处的 Ctrl。
触发要求完全一致:绑了 Ctrl+K,只按 K 不会触发,按 Ctrl+Shift+K 也不会。否则两个 App 分别绑这两个组合时,后者会连着前者一起响。
组合已被别的 App、或被原版/别的模组的键位占用时,该行变为「再按一次强制」并写明占用者;再按同一组合即强制绑定,此后该行常驻一个感叹号警示。
手机已经打开时按这些键不会切换 App——那时玩家多半正在手机里打字。
把任意尺寸的 PNG 放进 config/mcphone/wallpapers/,在这一页中选择。右上角「打开文件夹」用系统文件管理器直接打开该目录;点过之后这一页会盯着目录,拖进去的图切回游戏即可看见,无须退出重进。
把整个手机放大或缩小:拖动滑条,或点 − + 每档 25%。
| 项 | 值 |
|---|---|
| 范围 | 75% – 300% |
| 默认 | 100% |
| 配置键 | uiScale |
调整时手机(连同这一页本身)实时跟着变,无须另外预览。
配置键 uiScaleSnap。字体是位图、按最近邻绘制,因此每个像素是否方正只取决于一个乘积:GUI 缩放 × 界面大小。是整数则横平竖直;不是整数则有的列占两像素、有的占三像素,细线时粗时细。开启时档位自动对齐到 1 / GUI缩放:
| GUI 缩放 | 档位 |
|---|---|
| 2 | 100 / 150 / 200 … |
| 3 | 100 / 133 / 166 … |
| 4 | 100 / 125 / 150 … |
− + 一次正好走一档。关闭后可拖到任意值,代价是上述不匀。
六个预设,配置键 fontColor:
| 预设 | 值 |
|---|---|
| 白色(默认) | WHITE |
| 黑色 | BLACK |
| 琥珀 | AMBER |
| 天青 | CYAN |
| 薄荷 | MINT |
| 樱粉 | PINK |
用了浅色壁纸就换黑色,否则浅底浅字看不清。选中即生效,无须重启。
给自己这部手机起名。起过名的手机在物品栏显示该名。
| 项 | 值 |
|---|---|
| 长度上限 | 24 字符,服务端做校验与截断 |
| 铁砧改名 | 优先级更高,会覆盖设备名 |
手机屏幕只有 120 像素宽,24 字已远超能显示的量。
手机拿在副手里时自动挂到画面上。开关、大小与位置在这一页,也可以就地摆——拖边框或状态栏挪位置,滚轮缩放。完整说明见 副手 HUD。
以上标注「客户端」的设置全部存于 config/mcphone-client.toml,跟随这台电脑,换服务器、换存档均保留。也可从模组列表的「配置」按钮进入修改。
| 键 | 默认 | 对应界面 |
|---|---|---|
| fontColor | WHITE | 字体颜色 |
| musicMode | LIST_LOOP | 音乐的循环模式 |
| musicVolume | 100 | 音乐 App 音量,0–100 |
| appHotkeys | 空 | 每个 App 一个快捷键,一条写成 <App id>=<键> |
| cameraSoftFlash | false | 快门闪光,true 为模糊 |
| uiScale | 100 | 界面大小,75–300 |
| uiScaleSnap | true | 贴合清晰倍数 |
| hudEnabled | true | 副手 HUD 总开关 |
| hudAnchor | BOTTOM_RIGHT | HUD 贴哪个角,九选一 |
| hudOffsetX / hudOffsetY | 0 / 0 | HUD 从锚点再往里挪多少像素 |
| hudScale | 60 | HUD 上那部手机多大,40–150 |
服务端那一份(serverconfig/mcphone-server.toml)见服主须知 → 服务端配置。
手机里的输入框使用的就是原版按 T 那个聊天框的同一个控件,能否输入中文与原版完全一致。
界面上的每个视觉元素都可以用贴图替换。放了贴图就用贴图,没放就用内置配色,功能完全不受影响——不会出现缺图变紫黑格的情况。
把 PNG 放进资源包的 assets/mcphone/textures/,路径按下面几张表(相对该目录)。改完按 F3+T 重载资源包即可看到效果,无须重启游戏。下表共 44 个贴图位,与代码中的 PhoneSkin.Element 枚举一一对应。
| 文件名 | 画的是什么 | 建议尺寸 | 没放时 |
|---|---|---|---|
| phone/frame.png | 手机外壳边框 | 136×216(含边框的整机)。中间 120×200 必须透明 ——外壳画在最上层,不透明会糊掉整个屏幕;反之,绘制在中间区域的内圆角、刘海会正常显示 | 画纯色边框 |
| phone/status_bar.png | 顶部状态栏底 | 120×10 | 纯色 |
| phone/nav_bar.png | 底部导航栏底 | 120×14 | 纯色 |
| phone/nav_back.png | 导航栏返回键 | 40×14 | 画 ◁ 字符 |
| phone/nav_home.png | 导航栏主页键 | 40×14 | 画 ○ 字符 |
| phone/nav_tasks.png | 导航栏多任务键 | 40×14 | 画 □ 字符 |
| phone/drop_slot.png | 主屏拖动排序时「松手落这儿」的空槽 | 20×20 | 半透明纯色 |
| phone/page_dot.png | 主屏底部的页码点(不是当前页) | 3×3 | 半透明纯色 |
| phone/page_dot_active.png | 页码点(当前页) | 3×3 | 纯白 |
| phone/page_edge.png | 拖着图标停在屏幕边上时的翻页提示条 | 10×176(竖条) | 半透明纯色 |
| phone/toast.png | 收到消息时右上角的通知底 | 160×32(原版通知的槽位尺寸,照这个画才不会和其他模组的通知挤在一起错位) | 纯色加一圈描边 |
| phone/unread_badge.png | 未读条数的角标底。两处共用:会话列表里的未读数与消息通知右上角的计数是同一张图,换一次两处都变 | 12×9 | 纯色 |
| 文件名 | 画的是什么 | 建议尺寸 | 没放时 |
|---|---|---|---|
| chat/bubble_self.png | 自己发出的聊天气泡底(只有文字消息用,图片不套气泡) | 随内容拉伸 | 纯色 |
| chat/bubble_peer.png | 对方发来的聊天气泡底(同上) | 随内容拉伸 | 纯色 |
| chat/input_bar.png | 会话界面输入栏底 | 90×14 | 纯色 |
| chat/teleport.png | 会话列表里在线好友那一行右下角的「传送到他身边」小图标 | 7×7(按实际绘制尺寸画,这里不做平滑缩放) | 画 → 字符 |
| chat/attach.png | 会话界面输入栏左边那个「+」(点开是图片 / 表情) | 9×9 | 画 + 字符 |
| 文件名 | 画的是什么 | 建议尺寸 | 没放时 |
|---|---|---|---|
| music/prev.png | 「上一首」 | 9×9 | 画 ⏮ 字符 |
| music/play.png | 「播放」 | 9×9 | 画 ▶ 字符 |
| music/pause.png | 「暂停」 | 9×9 | 画 ⏸ 字符 |
| music/next.png | 「下一首」 | 9×9 | 画 ⏭ 字符 |
| music/eject.png | 唱片仓的「取出」 | 9×9 | 画 ⏏ 字符 |
| music/backpack.png | 唱片仓的「从背包放」(只在仓是空的时候出现) | 9×9 | 画 ▤ 字符 |
| music/mode_list_loop.png | 循环模式:列表循环 | 9×9 | 画 ↻ 字符 |
| music/mode_single_loop.png | 循环模式:单曲循环 | 9×9 | 画 ① 字符 |
| music/mode_shuffle.png | 循环模式:随机播放 | 9×9 | 画 ⇄ 字符 |
| 文件名 | 画的是什么 | 建议尺寸 | 没放时 |
|---|---|---|---|
| gallery/delete.png | 相册单张查看里的「删除」键(点过一次之后变成文字「再点一次确认」,那一态不走贴图) | 9×9 | 画「删除」两个字 |
| store/button.png | 应用详情页上可点的按钮底(购买 / 下载) | 100×16 | 纯色 |
| store/button_disabled.png | 点不动时的按钮底(已安装 / 买不起) | 100×16 | 纯色 |
| store/companion.png | 商店里「联动App」入口格的图标 | 20×20 | 纯色底加三个小方块 |
store/button_disabled.png 是独立的一张,而非把可点的那张自动调暗:「不可点」该长什么样,由绘制贴图的人决定。
| 文件名 | 画的是什么 | 建议尺寸 | 没放时 |
|---|---|---|---|
| browser/panel.png | 浏览器那块大面板的底 | 320×200 | 纯色 |
| browser/bar.png | 浏览器工具条那一条的底 | 320×22 | 纯色 |
| browser/back.png | 「后退」键 | 22×18(整个键框,不是居中的小图标) | 画 ◀ 字符 |
| browser/forward.png | 「前进」键 | 22×18 | 画 ▶ 字符 |
| browser/reload.png | 「刷新」键 | 22×18 | 画 ↻ 字符 |
browser/panel.png 绝大部分会被网页盖住,真正看得见的只有加载中的空白期,纯色就够。browser/bar.png 那一条在面板外面,浮在面板上方的留白里,不占网页高度。
| 文件名 | 画的是什么 | 建议尺寸 | 没放时 |
|---|---|---|---|
| reader/book.png | 书架列表里那本兜底的书(只在书源画不出那本书自己的图标时用得上) | 16×16(按实际绘制尺寸画,这里不做平滑缩放) | 纯色 |
| reader/search_bar.png | 阅读页顶上那条搜索栏的底 | 112×12 | 纯色 |
| reader/shelved.png | 书已收进书架时行右端那颗星 | 9×9(按实际绘制尺寸画) | 画 ★ 字符 |
| reader/unshelved.png | 书还没收进书架时那颗星 | 9×9(按实际绘制尺寸画) | 画 ☆ 字符 |
| reader/tab.png | 底部「书架 / 书城」当前那一页的底 | 54×12 | 纯色(另一页不画底) |
| 文件名 | 画的是什么 | 建议尺寸 | 没放时 |
|---|---|---|---|
| settings/slider_track.png | 滑条的槽(「界面大小」那一条,将来的音量条也用它) | 100×6,整张横向拉伸 | 纯色 |
| settings/slider_fill.png | 滑条已填充的那一段 | 100×6 | 纯色 |
| settings/slider_knob.png | 滑块 | 4×14(比槽高,压在槽上) | 纯色 |
| settings/step_button.png | 滑条两端 − + 键的底 | 14×14 | 纯色,悬停时提亮 |
这四个位刻意使用通用名称:此后其他滑条(如音量)复用同一组贴图,只需绘制一次。
App 图标单独放在 app/ 下,文件名就是 App 的短名,都是 20×20:
app/settings.png app/app_store.png app/chat.png app/camera.png
app/gallery.png app/music.png app/ender_chest.png app/notes.png
app/waystone.png app/browser.png app/clock.png app/weather.png
app/reader.png app/quests.png附属模组的 App 图标路径由它自己决定,见附属接口文档。
天气页那张大图标在 weather/ 下,六张 32×32:clear.png、rain.png、snow.png、thunder.png、dry.png、none.png,按当前是什么天各显其一。
尺寸不必精确匹配,贴图会被拉伸到目标区域。要不变形,按建议尺寸或其等比放大绘制即可。
模组自带图的倍数:
| 贴图 | 倍数 |
|---|---|
| phone/frame.png | 2 倍(272×432) |
| 导航栏三个键 | 10 倍(400×140) |
| 书架两颗星、浏览器三个键、滑块与 − + 键 | 4 倍 |
放大倍数取整数 ——非整数倍在低 GUI 缩放下会掉线条。
默认整张拉伸。不带元数据的贴图会被拉到实际大小,带圆角的图在宽气泡上抻长、在窄气泡上压扁,因此纯色或纵向渐变最稳妥。
要让圆角不变形,在 PNG 旁放一份同名的 .png.mcmeta:
{"mcphone_skin": {"border": 3}}该图即改走九宫格:四角按源图上那 3 个像素原样绘制,只有四条边和中间被拉伸,气泡无论多宽多高,四个角都一样大。
| 情形 | 行为 |
|---|---|
| border 大于源图短边的一半 | 忽略,退回整张拉伸 |
| 老资源包未写该文件 | 行为不变,无须改动 |
自带图中使用九宫格的有五张:
| 贴图 | border |
|---|---|
| chat/bubble_self chat/bubble_peer chat/input_bar | 3 |
| settings/slider_track settings/slider_fill | 2(使两端圆头不随条一起抻长) |
因此模组在有贴图时改用别的表达:
| 状态 | 表达 |
|---|---|
| 导航键、商店按钮悬停 | 整张按倍数提亮(资源包画的是什么颜色,亮起来还是那个颜色) |
| 浏览器三个键点不动 | 整张压暗 |
| 音乐键、聊天的「+」 | 把高亮块画在贴图底下 |
绘制贴图时不必自行表达这些状态。半透明有效:alpha 介于 0 与 255 之间的像素会正常混合,抗锯齿边缘、整块半透明的底(「壁纸上压一层暗色」这类效果)都可以画。1.7.40 之前这类像素会被当作不透明绘制,边缘发硬、半透明的底变成实心。那是模组的缺陷,不是贴图的问题。
| 情形 | 行为 |
|---|---|
| 文件不是合法 PNG | 退回纯色并在日志记一条警告,不崩溃 |
生成完整清单(含每个位现有的图长什么样、缺图时退回哪个颜色):
python3 docs/make_texture_manifest.py输出一张 HTML。不在文档里存一份静态清单,是因为那种清单必然过期——PhoneTheme 头部那一份就是这么失效的。
服务端配置、图片占多少硬盘、备份要备哪些目录。
文件位置:
| 环境 | 路径 |
|---|---|
| 单人游戏 | 存档目录下的 serverconfig/mcphone-server.toml,每个存档一份 |
| 专用服务器 | serverconfig/mcphone-server.toml |
| 键 | 默认 | 范围 | 说明 |
|---|---|---|---|
| allowFriendTeleport | true | — | 关闭后好友那一行的传送图标不再显示,服务端也拒绝传送请求。好友关系与聊天不受影响 |
| allowChatImages | true | — | 关闭后输入栏左侧的图片键不再显示,服务端也拒收上传。已发送的图不删除,仍然可见 ——关掉的是「再发新的」 |
| chatImageMaxKb | 512 | 64–768 | 单张图片的 KB 上限。客户端按它压缩,服务端按它接收 |
每对会话最多保留 20 张不同的图,所以每对好友封顶是该值的 20 倍。默认 512 KB 即每对 10 MB;200 对常聊的好友是 2 GB 上限。
图片按内容存储(文件名为「会话键 + 图片字节」的哈希),因此同一张表情在同一对好友之间发一百次,硬盘上仍然只有一份,也只占那 20 个名额中的一个。
| 想怎么调 | 结果 |
|---|---|
| 调小 chatImageMaxKb | 客户端自动降一档尺寸重压,图变糊;动图先掉帧、再不行退成一张静态图。功能不坏,画质让路。128 KB 大致是每对 2.5 MB |
| 调大 | 图更清楚、动图更流畅,硬盘与带宽跟着涨 |
| 完全关闭 | allowChatImages = false,已发送的仍然可见 |
上限 768 KB 是硬的:再往上就顶到原版「服务端发给客户端」那 1 MB 的包上限。服务器启动时会扫描一遍图片仓,删除没有任何消息认领的文件(写完文件、消息尚未落盘就崩溃会留下这种孤儿)。
做备份时要知道:
| 数据 | 位置 | 随什么走 |
|---|---|---|
| 好友关系、聊天记录 | 世界存档的 data/ | 存档 |
| 聊天里发的图片 | 世界存档的 mcphone/chat-images/ | 存档 |
| 笔记、已购 App、唱片仓、终端卡槽、壁纸选择、未读标记 | 玩家数据(playerdata/) | 存档 + 玩家 |
| App 安装状态与主屏排列 | 客户端 config/mcphone/installed/<存档>.json | 玩家自己的客户端 |
| 书架收藏 | 客户端 config/mcphone/reader/shelf.json | 玩家自己的客户端,全局一份不分存档 |
| 表情包 | 客户端 config/mcphone/stickers/ | 玩家自己的客户端,全局一份不分存档 |
| 客户端设置(字色、界面大小、快捷键、HUD) | 客户端 config/mcphone-client.toml | 玩家自己的客户端 |
MCphone 会在方块注册的末尾,把 Integrated Dynamics 全部方块的掉落表提前解析一次。
成因:NeoForge 的注册阶段只要收到任何一个模组抛出的异常,就会把整个注册表回滚成原版状态;而回滚过程本身会在 Integrated Dynamics 的墙上火把处踩到一个空指针,于是真正的错误被顶掉,崩溃报告上只剩一句
Trying to access unbound value: integrateddynamics:menril_torch_stone看起来像是 Integrated Dynamics 的问题,实际上它只是最后一个倒下的。提前解析一次即可绕开那个空指针,使回滚正常完成并抛出真正的原因。
细节写在 compat/IntegratedDynamicsCompat.java 的类注释里。未安装该模组时,这段代码一行都不会执行。
由第三方开发、基于 MCphone 的 App SPI 构建的模组。本页只做索引。
第三方模组索引 · 独立发布
| 附属 | 作者 | 做什么 | 许可 |
|---|---|---|---|
| mcphone-deepseek | november521 | 手机里的 DeepSeek 对话 App,纯客户端 | MIT |
开一个 Issue 说明下列各项即可,或直接改本页提 PR:
| 字段 | 要求 |
|---|---|
| 仓库地址 | 公开可访问 |
| 做什么 | 一句话,说清它在手机里加了什么 |
| 许可 | 仓库里要有 LICENSE |
| 适配范围 | 支持哪些 Minecraft 版本与加载器 |
长期无法构建、或仓库已删除的条目会从本页移除。
从三分钟做一个 App 开始,四步:加依赖、实现 IPhoneApp、登记 SPI、放语言文件与贴图。
给附属模组作者:做一个手机 App,画进屏幕,给它定价。
本文档分几页,按你需要的深度挑着看:
所有接口都在 dev.november.mcphone.api 及其子包,命名以 I 开头。
| 类型 | 所在包 | 用途 |
|---|---|---|
| IPhoneApp | dev.november.mcphone.api.app | 画在手机屏幕上的那个 App,主入口 |
| IPhonePage | dev.november.mcphone.api.app | App 内部的一个界面(主屏 → 列表 → 详情) |
| IAppSource | dev.november.mcphone.api.store | 把一组 App 报告给商店与主屏 |
| AppInfo | dev.november.mcphone.api.store | 一个 App 的描述(id、名字、图标、简介、价格) |
| AppEntry | dev.november.mcphone.api.store | AppInfo + 创建 IPhoneApp 的工厂 |
| ICost | dev.november.mcphone.api.cost | 价格家族的根接口 |
| Cost.Free | …cost.builtin | 免费 |
| Cost.Item | …cost.builtin | 用物品付 |
| Cost.Items | …cost.builtin | 用一组物品付 |
| ICostRegistry | …cost | 注册自己的价格类型 |
| IAppPriceProvider | …cost | 把定价来源整个换掉 |
| PhoneApi | dev.november.mcphone.api | 取单例、判版本、判某 App 在不在 |
| PhoneTextures | …api.ui | 手机内部的共享绘制原语 |
| ScrollSpec | …api.ui | 滚动列表怎么滚(快慢、回弹、边距) |
| INotifyHandler | …api.notify | 通知 API |
| ApiVersion | …api | API 版本号,用于版本与两端安全 |
下文按包逐一说明。每节都给最小代码;照抄即可跑,不必先读完整份文档。
MCphone 通过 Java 的 ServiceLoader 发现附属:你的 jar 里要有一个 META-INF/services/dev.november.mcphone.api.store.IAppSource 文件,里面一行是实现类的全名。
com.example.mymod.ExampleAppSourceMCphone 启动时会加载所有这样的实现,调用 apps(),把返回的 AppEntry 合并进主屏与商店。不需要事件、不需要注解处理器,一个文本文件搞定。
有。november521/mcphone-deepseek 是一个真实发布的附属,代码不长,结构就是「一个 IAppSource、一个 AppInfo、一个 IPhoneApp、一张列表页 + 一个详情页」。读它比从头猜快。
更短的起步模板见五分钟做一个 App。
| 你想… | 先读 |
|---|---|
| 尽快出一个能点开的 App | 五分钟做一个 App |
| 搞懂屏幕上的生命周期与输入 | IPhoneApp |
| 做列表页 / 详情页那种层级 | IPhonePage |
| 让 App 出现在主屏与商店 | IAppSource |
| 给 App 定价 | ICost 家族 |
| 处理服务端 / 客户端的边界 | 版本与两端安全 |
| 做浏览器后端 | 浏览器后端 |
| 少走弯路 | 非踩不可的坑 |
做出来之后,想让它被更多人知道,见附属一览的收录规则。
四步:加依赖、实现 IPhoneApp、登记 SPI、放语言文件与贴图。
MCphone 1.10 · Minecraft 1.21.1
发版时逐个目标构建,全部 jar 挂载于同一个 Release,文件名形如 mcphone-<版本>-<目标>.jar。取与自身目标一致的一个 —— 当前仅 mcphone-<版本>-1.21.1-neoforge.jar 一项可用。
下文以 NeoForge 为例,构建插件为 ModDevGradle,而非 ForgeGradle:不存在 fg.deobf 一类写法。
dependencies {
compileOnly files("libs/mcphone-1.10.1-1.21.1-neoforge.jar")
// 想在开发环境里真跑起来,再加一行
localRuntime files("libs/mcphone-1.10.1-1.21.1-neoforge.jar")
}
在 neoforge.mods.toml 中声明依赖,使加载顺序正确:
[[dependencies.yourmod]]
modId = "mcphone"
type = "required" # 可选依赖写 "optional"
versionRange = "[1.2.13,)" # 用到 IPhonePage 即为这一版起;只做图标可写 [1.0.46,)
ordering = "AFTER" # 必须 AFTER:要用的注册表得先就位
side = "BOTH"
| 项 | 当前值 |
|---|---|
| Minecraft | 1.21.1 |
| NeoForge | 21.1.200+ |
| MCphone | 1.10.0 |
public final class CalculatorApp implements IPhoneApp {
@Override
public ResourceLocation getId() {
return ResourceLocation.fromNamespaceAndPath("mymod", "calculator");
}
@Override
public Component getDisplayName() {
return Component.translatable("mymod.app.calculator");
}
@Override
public ResourceLocation getIconTexture() {
return ResourceLocation.fromNamespaceAndPath("mymod", "textures/app/calculator.png");
}
@Override
public void onPress() {
Minecraft.getInstance().setScreen(new CalculatorScreen());
}
}
这四个方法是必须实现的,其余全为 default,见 方法一览。
界面要画在手机屏幕内部 而不是跳出去时,改为覆盖 openPage()。
文件:src/main/resources/META-INF/services/com.november.mcphone.api.client.app.IPhoneApp
内容:com.yourmod.client.CalculatorApp
文件名就是接口全名,内容是实现类全名,一行一个。
{
"mymod.app.calculator": "计算器"
}
英文另开 en_us.json,两边的键要对齐。
20×20、PNG-32。路径要与 getIconTexture() 返回的一致
一个 App 是怎么被手机调起来、画出来、关下去的。
dev.november.mcphone.api.app.IPhoneApp 是一个 App 的运行态——玩家在主屏上点了你那个图标之后,手机实例化的那个对象。它不是 App 的描述(描述是 AppInfo),描述不变,运行态每次开机都是新的。
| 它管 | 管不管 | 由谁管 |
|---|---|---|
| 名字、简介、图标、价格、id | 不管 | AppInfo / AppEntry |
| 装没装、出不出现在主屏 | 不管 | IAppSource 与商店 |
| App 当前画哪一页 | 管 | currentPage(),见IPhonePage |
| 开机拉数据、关机写盘 | 管 | onOpened() / onClosed() |
| 屏幕上画出来什么 | 管 | render() |
| 时机 | 回调 | 做什么 |
|---|---|---|
| 玩家点主屏图标 | onOpened(player) | 拉数据、开第一页、初始化滚动位置 |
| 每帧 | render(pose, mx, my, pt) | 画当前页 |
| 玩家点机身外 / 按 ESC | onClosed() | 写盘、丢引用、不在这里关连接(已随世界卸载断开) |
关 App 与换世界、登出的顺序是:先 onClosed(),再丢引用、再卸载世界。网络连接会随世界卸载自动断开,不要在 onClosed() 里再关一次——那种「先关连接、再清缓存」的顺序会让卸载流程等连接,或在空引用上再关一次。
进 App 后,你有多少数据,就开多少页:
| 你的情况 | 开机后应该开哪一页 |
|---|---|
| 本来就有东西(已配置、有历史) | 直接开那条数据的详情页 |
| 什么都没有(首次安装、刚配置完) | 开列表页,并把空状态画在列表页上 |
不要「空数据时开一个假的空状态页」:那个假页没有返回栈、没有可点的东西,玩家只能走导航栏 ◁,而且连进 App 时看到的第一屏和正常用完全不一样。空状态应该画在正常会用到的那一页上——列表为空、详情加载中——那一页照样能翻、能点新建、能返回,只是内容区是一段提示。
手机屏幕内部是 120 × 200 的坐标空间(外框另算)。所有 render() 的坐标都在这个空间里:
| 区域 | 坐标 |
|---|---|
| 顶部状态栏 | y = 0 … 10 |
| 正文区 | y = 12 … 186 |
| 底部导航栏 | y = 188 … 200 |
| 正文宽度 | x = 0 … 120 |
正文区域之外是手机自带的状态栏与导航栏,别往里画——你的内容会被它们盖住,或者反过来盖住它们。
手机里那些按钮、滑条、滚动条不是每个 App 各自画的,而是一套共享控件,放在 dev.november.mcphone.api.ui.PhoneTextures 与相关的 …ui 类里:
| 类 | 画什么 |
|---|---|
| PhoneTextures | 按钮底(正常 / 悬停 / 禁用)、滑条(槽 / 填充 / 滑块)、九宫格圆角块 |
| ScrollSpec | 滚一滚有多快、有没有回弹、内容区上下留多少 |
用同一套控件,而不是自己画一份。否则换肤只能换一部分——资源包作者改了按钮贴图,你的 App 还是老样子,界面一半新一半旧。
换肤的完整路径表见 用户文档;从 API 这边调用它们的方式,看 mcphone 源码里相机 / 相册那两个内置 App 怎么画的,照着调即可。
玩家换了壁纸、改了字色,你的 App 不必自己跟着改:
你的 App 依赖某个别的模组(例如 Waystones、MCEF、NetMusic),实现一个 companionMods() 方法,返回那些 modid:
| 情况 | 商店的行为 |
|---|---|
| 依赖的模组装了 | App 正常出现在主屏与商店列表里 |
| 依赖的模组没装 | 主屏与商店列表里不出现,但商店的「联动App」页会把它列出来,写明缺哪个模组,整行压暗、看得见点不动 |
它回答的是「我怎么没有这个 App」。两个方法的区别:
| 方法 | 含义 |
|---|---|
| requiredMods() | 缺了它 App 就根本不能用,不进主屏,但联动页里列出来 |
| companionMods() | 缺了它 App 仍能跑,只是少一块功能;同样进联动页 |
两者都收进联动页,当前可用的 App 不列——那是噪声。
你的 App 想跳到另一个 App 的某个界面(例如从设置跳到壁纸选择、从聊天跳到相册),用 PhoneApi.get().openPage(...):
| 要点 | |
|---|---|
| 走接口调用,不直接 new | 跨模组 new 对方的类会在对方卸载时崩;走 PhoneApi 的查找,找不到就返回空,你的 App 退一格 |
| 目标 App 得在 | 对方的 App 没装 / 被禁用时,调用静默失败,不抛异常 |
| 返回栈 | 对方页打开后按 ◁ 回到你的页,不是直接回主屏 |
主屏 → 列表 → 详情,这种层级是怎么做出来的。
dev.november.mcphone.api.app.IPhonePage 是 App 内部的一个界面。App 本身是「当前在第几页」,IPhonePage 是那一页长什么样、收什么输入。
| IPhoneApp | IPhonePage | |
|---|---|---|
| 实例 | 每次开机新的 | 可以是单例(静态常量),也可以带参数 new 出来 |
| 关心 | 当前是哪一页、整体生命周期 | 这一屏画什么、点哪里、滚哪里 |
| 返回键 | 不直接处理 | 由它决定:能不能退、退到哪 |
屏幕底部的 ◁ 是手机自带的,玩家按它或点它,手机会问「当前页能不能退」:
| 你的返回栈 | ◁ 的行为 |
|---|---|
| 详情页 | 弹栈,回到列表页 |
| 列表页(App 的根页) | 整个 App 关闭,回主屏 |
| 主屏 | 不响应(主屏没有上一层) |
你只管维护自己那一页的栈(一个 Deque<IPhonePage> 或两个字段足矣),不要自己在屏幕上画返回键——和底部那条自带导航会重复。
ESC 是手机级快捷键:它不分层级,按下去整个手机直接关机,不会先走 ◁。因此你的页面里不需要为「按 ESC」写处理。
在 App 内部换页,就是把 currentPage() 返回的对象换掉:
private IPhonePage current = ListPage.INSTANCE;
public IPhonePage currentPage() {
return current;
}
// 玩家点了列表里某一项
public void openDetail(Item item) {
this.current = new DetailPage(item, this::backToList);
}
private void backToList() {
this.current = ListPage.INSTANCE;
}DetailPage 持有一个「返回时该做什么」的回调,比让它直接持有 App 引用干净——对方将来改名 / 拆包时,你的详情页不会跟着断。
一页里有很多条,滚不完怎么办:用 ScrollSpec 描述怎么滚,把滚动位置存在你自己的字段里:
| 它管 | 值 |
|---|---|
| 一次滚多少 | 滚轮一格的步长 |
| 回弹 | 滑过头之后拉回多少 |
| 上下边距 | 正文区里留多少不画内容,避免贴到状态栏 / 导航栏 |
滚多远只在当前实例里:换页时新的页从零开始滚。不要把滚动位置存进存档——那是会话级的状态,不是持久数据。
你的 App 想在屏幕右上角弹一条通知(新消息、任务完成),用 dev.november.mcphone.api.notify.INotifyHandler:
| 要点 | |
|---|---|
| 走单例,不 new | 通过 PhoneApi 取当前处理器 |
| 不存图、不存字 | 通知是一帧的事,弹完就丢;要持久化的内容请放进 App 自己的页 |
| 点了通知 | 由你决定跳哪一页;手机只负责弹与消失 |
聊天 App 的未读条数与「你被传送了」那种系统提示共用同一个槽位,别刷屏——同一事件连发十条,玩家只会觉得手机坏了。
一个 App 的静态描述:id、名字、图标、简介、价格,以及把它交给手机的注册来源。
「一个 App 是什么」和「一个 App 现在跑成什么样」是两件事:
| AppInfo(描述) | IPhoneApp(运行态) | |
|---|---|---|
| 生命周期 | 整个游戏都在,只有一份 | 每次开机新建,关机即丢 |
| 内容 | id、名字、图标、简介、价格、作者 | 当前页、滚动位置、拉到的数据 |
| 谁创建 | 附属模组在启动时写死 | 手机在玩家点图标时 new |
商店列表、主屏图标、「设置 → App 管理器」的详情,全都来自 AppInfo;点进去之后才轮到 IPhoneApp。
public static final AppInfo INFO = AppInfo.builder(
"yourmodid", "example") // 命名空间 + id,全局唯一
.displayName("示例") // 主屏与商店显示的名字
.icon(ResourceLocation.fromNamespaceAndPath(
"yourmodid", "icon/example.png")) // 20x20
.summary("一个最小的示例 App") // 商店详情里的一句话
.cost(new Cost.Item(Items.DIAMOND, 1)) // 见 ICost 家族
.build();| 字段 | 要求 |
|---|---|
| id | 命名空间用你自己的 modid,不要用 mcphone——那是内置 App 的地盘,将来加新 App 会撞名 |
| displayName | 短,主屏一格放不下长句 |
| icon | 20×20 PNG,没有兜底,路径错就是空白格 |
| summary | 商店详情里那一行,说明这 App 干什么 |
| cost | 见ICost 家族 |
光有描述还不够——手机还得知道「点下去 new 哪个类」。AppEntry 就是把 AppInfo 和一个创建运行态的工厂绑在一起:
public class ExampleAppEntry implements AppEntry {
@Override
public AppInfo info() { return ExampleAppInfo.INFO; }
@Override
public IPhoneApp create() {
return new ExampleApp(); // 每次开机新实例
}
}一个附属可能有多个 App(例如「小工具」下挂着记事本、末影箱、传送石三个)。IAppSource 就是「我这一个附属有哪些 App」的清单:
public class ExampleAppSource implements IAppSource {
@Override
public List<AppEntry> apps() {
return List.of(
new ExampleAppEntry(),
new AnotherAppEntry()
);
}
}这个类同时是 SPI 文件里那一行的指向:
com.example.yourmod.ExampleAppSource| 规则 | 行为 |
|---|---|
| 新装的 App 排在哪 | 主屏最后一格,不打乱已有排列 |
| 两个附属报了同一个 id | 先加载的赢,后加载的记一条日志,不崩 |
| 你的 App 在商店里排第几 | 按附属加载顺序,不保证固定;别在介绍里写「排在第 N 个」 |
App 卖多少钱:免费、用物品、用一组物品,以及把整套定价换掉。
有的 App 替代了一件原版实物:末影箱那格卖一个末影箱,传送石那格卖一个传送石。手机本身没花钱就能用,但这种 App 一装上,那台实物你以后永远用不上了——收一份对应的物品,等于把原来的成本还给玩家。
反过来,终端那格免费:它开的是玩家自己那台终端,没有终端它一格内容都没有。真正的门槛是那台终端,再收一次等于收两遍。
| 类 | 怎么写 | 商店里长什么样 |
|---|---|---|
| Cost.Free | new Cost.Free() | 「免费」 |
| Cost.Item | new Cost.Item(Items.DIAMOND, 1) | 一颗钻石 |
| Cost.Items | new Cost.Items(Map.of(Items.DIAMOND, 2, Items.EMERALD, 1)) | 两件并排 |
买不起时商店按钮置灰并写明缺什么。购买走原版消耗:扣物品、放进去、写进存档,全程服务端判定,玩家不能伪造客户端绕过。
你想要「用经验等级付」「用服务器积分付」这种 MCphone 没想过的价格,实现 ICost,再通过 ICostRegistry 注册:
public class ExampleLevelCost implements ICost {
private final int levels;
public ExampleLevelCost(int levels) { this.levels = levels; }
@Override
public boolean canPay(Player player) {
return player.experienceLevel >= levels;
}
@Override
public void pay(Player player) {
player.giveExperienceLevels(-levels);
}
}商店要把它画出来时,会问 displayText()——返回「12 级经验」这一行字即可。判定与扣除都在服务端,客户端只负责显示与提示。
有的整合包有自己的经济系统,不想让玩家用物品付。IAppPriceProvider 就是把「价格从哪来」整个换掉:
| 层 | 职责 |
|---|---|
| App 自己声明的 cost | 默认来源,写在 AppInfo 里 |
| IAppPriceProvider | 覆盖者:在它眼里这个 App 值多少、怎么付 |
内建的 feature/store/BuiltinAppPrices 就是默认来源,声明了末影箱、传送石那几个价格。整合包想全部免费,写一个 Provider 把所有 App 都报成 Cost.Free 即可,不必改 MCphone、也不必改每个附属。
API 怎么保证不悄悄崩,以及服务端 / 客户端的边界在哪。
API 的版本号由 ApiVersion 给出,按下面的规矩演化:
| 变更类型 | 对已发布的附属 |
|---|---|
| 加新方法(带默认实现)、加新包、加新类 | 不破坏,附属照跑 |
| 改方法签名、删类、改语义 | 会升大版本,旧的标记 @Deprecated 保留一个版本再删 |
| 渲染坐标、颜色常量 | 属实现细节,可能变;别在自己代码里写死 120×200 之外的数 |
mcphone 与附属的版本对不上时,游戏启动时会在日志里给出明确提示(哪个附属要求哪个 API 版本、当前 mcphone 是哪个),而不是进游戏点开某个 App 才崩。
| 动作 | 必须在 |
|---|---|
| 扣物品、判付费成功 | 服务端。客户端发个包「我付了」会被服务端再验一次 |
| 写玩家数据、写存档 | 服务端 |
| 传送玩家、改游戏规则 | 服务端 |
| 画屏幕、读鼠标、动画 | 客户端。服务端没有屏幕 |
| 查「玩家现在开着哪个 App」 | 各自知道各自的,跨端要发包 |
规则很简单:凡是影响游戏世界的,都得在服务端判定;凡是只画给眼睛看的,都在客户端。中间要传的信息用 NeoForge 的网络包,别图省事在客户端直接改玩家血量——那只改了你自己屏幕上的数,一帧就被服务端盖回去。
因此你的 App 代码不能在类加载时就碰渲染相关的东西(GL 状态、字体渲染器、屏幕宽高)。要画的东西放在 render() 里,那个方法只在客户端被调到;类定义、静态字段、AppInfo 的构造里都不要 new 字体、不要拿 Minecraft.getInstance().font。
否则纯服务端一开服就 NoClassDefFoundError。
浏览器那块大面板底下用什么加载网页,是一个可替换的扩展点。
MCphone 内置的浏览器用 MCEF 加载网页。它在 MC 生态里是最成熟的选择,但有两个已知限制,写在 用户文档 里:
这两条都来自 MCEF 本身,MCphone 改不了。
将来你想用别的浏览器内核(比如另一个 CEF 封装、或者一个离线文档服务器),实现浏览器后端的扩展点:
| 你要实现 | 做什么 |
|---|---|
| 创建 / 销毁后端 | 打开面板时起,关闭时收 |
| 加载一个 URL | 地址栏敲完、回车 |
| 绘制到面板 | 给一块纹理,手机把它贴上去 |
| 转发鼠标 / 键盘 | 点击、滚动、输入 |
手机只认「一块纹理 + 一组输入事件」,不关心底下是 CEF 还是什么。后端选哪个由运行环境决定,不要在自己的附属里硬编码依赖 MCEF——玩家可能换了后端。
| 状态 | 归谁 |
|---|---|
| 历史记录、书签、缓存 | 后端自己 |
| 面板大小、工具条位置 | 手机 |
| 当前 URL、加载中圆点 | 后端报给手机,手机画在工具条上 |
后端不必知道手机的坐标系;它只要把一张固定尺寸的纹理画好,手机负责把它贴到面板上、跟着界面大小缩放。
做附属最常犯的五个错,每个都给了现场和修法。
icon() 返回的 ResourceLocation 是 assets/<命名空间>/textures/… 之下的相对路径。最常见的错是写成 "icon/example.png" 而文件放在 assets/yourmodid/textures/gui/icon/example.png——命名空间对,路径少了一层。用原版的 getResourceLocation 检查一下,或直接看 mcphone 内置 App 的图标是怎么放的。
正文区只有 y = 12 … 186。按钮放在 y = 190 就压在导航栏上了;放在 y = 5 就顶到状态栏。滚列表时用 ScrollSpec 留的上下边距,别自己硬算。
凡是影响游戏世界的,都得发网络包到服务端判定。客户端改的只是本地副本,服务端不承认。完整边界见版本与两端安全。
你的 AppInfo 是静态常量,它的构造在类加载时就跑。别在那里 new 字体、拿 Minecraft.getInstance().font、读屏幕宽高。要画的东西放在 render()。见纯服务端也能装。
不要写 "mcphone:camera" 这种 id 去找内置 App——要用就走 openPage() 的查找,找不到就静默退一格。坐标、颜色常量同理:用 PhoneTextures,不要自己画一份按钮底。
不需要分叉仓库。本模组以 MIT 许可发布,克隆后用 Gradle 打包即可。
git clone https://github.com/november521/mcphone.git
cd mcphone
./gradlew build产物在 build/libs/,文件名形如 mcphone-<版本>-<目标>.jar。
仓库不按 Minecraft 版本或加载器分支,全部目标位于 main。计划支持哪些目标、各自什么状态,写在 versions/targets.json;当前发布的构建只有 Minecraft 1.21.1 · NeoForge 一项。
用官方映射(MojMap)。跑客户端用 ./gradlew runClient,跑服务端用 ./gradlew runServer。