github.com/november521/mcphone
WIKI HOME

把一部能用的智能手机塞进 Minecraft

拍照、翻相册、换壁纸、听歌、聊天、装 App。

Minecraft 1.21.1 · NeoForge 21.1.200+ · 客户端与服务端都需安装。
DOWNLOAD

下载

第三方下载与发布渠道:

渠道说明
Modrinth下载 Modrinth 版
CurseForge下载 CurseForge 版
Releases本仓库发版,所有 jar 挂在同一 Release
Issues反馈问题
MANUAL

使用手册

页面内容
使用手册总览手册的分类索引与常见问题速查
快速开始可选依赖、工作台合成、开机与关机、主屏排列与翻页
副手 HUD手机挂在画面上:Alt 操作、G 叫出、摆位置与大小
内建 App 一览十五个 App 一张表
APPS

App

页面内容
拍照与相册取景与快门闪光、缩略图网格、删除
音乐曲库与唱片仓、NetMusic、格式与 MP3 限制
时钟与天气游戏内时间、六种天气与对应玩法
美西螈(聊天)好友、会话、发图与表情、GIF、传送到好友身边
小工具记事本 · 末影箱 · 传送石
浏览器地址栏兼搜索、MCEF、已知限制
阅读书架与书城、搜索、三种书源
任务书FTB Quests 联动
终端AE2 / RS / Tom's,终端卡槽
应用商店装卸 App、「联动App」页
CUSTOM

自定义与开服

页面内容
设置App 管理器、快捷键、壁纸、界面大小、字体颜色、设备名称
换肤(资源包)44 个贴图位、九宫格 .mcmeta、App 与天气图标
服主须知服务端配置、图片占多少硬盘、数据存在哪儿
VERSION

支持的版本

当前发布的构建仅有 Minecraft 1.21.1 · NeoForge 一项,本 wiki 的截图与操作说明均以其为准。其余 Minecraft 版本与加载器(Forge、Fabric)尚无可供下载的构建。

仓库不按 Minecraft 版本或加载器分支,全部目标位于 main。发版时逐个目标构建,所有 jar 挂载于同一个 Release,文件名形如 mcphone-<版本>-<目标>.jar

计划支持的目标及各自状态见 versions/targets.json。从源码构建见 从源码构建;本模组以 MIT 许可发布。

MANUAL

使用手册

把一部能用的智能手机塞进 Minecraft —— 拍照、翻相册、换壁纸、听歌、装 App,还能给自己那一部手机起个名。

Minecraft 1.21.1 · NeoForge 21.1.200+ · 客户端与服务端都需安装。
版本说明:当前发布的构建仅有 Minecraft 1.21.1 · NeoForge 一项,下文的截图与操作说明均以其为准。其余版本与加载器尚无可供下载的构建,计划与进度见 versions/targets.json
BEGIN

入门

页面内容
快速开始可选依赖一览、工作台合成、开机与关机、主屏排列与翻页
副手 HUD手机挂在画面上:Alt 操作、G 叫出、摆位置与大小
内建 App 一览十五个 App 一张表,各自去哪一页
APPS

App

页面内容
拍照与相册取景与快门闪光、缩略图网格、删除
音乐曲库(耳机)与唱片仓(外放)、NetMusic、格式与 MP3 限制
时钟与天气游戏内时间、六种天气与对应玩法
美西螈(聊天)好友、会话、发图与表情、GIF、传送到好友身边、上限
小工具记事本 · 末影箱 · 传送石
浏览器地址栏兼搜索、MCEF 的原生库、已知限制
阅读书架与书城、搜索、Patchouli / GuideME / 白名单三种书源
任务书FTB Quests 联动,为什么预装且免费
终端AE2 / RS / Tom's,终端卡槽
应用商店装卸 App、「联动App」页
CUSTOM

自定义

页面内容
设置App 管理器、每个 App 一个快捷键、壁纸、界面大小、字体颜色、设备名称
换肤(资源包)44 个贴图位、九宫格 .mcmeta、App 与天气图标
SERVER

开服与开发

页面内容
服主须知服务端配置、图片占多少硬盘、数据存在哪儿、备份
附属接口文档给附属模组作者:做一个手机 App
从源码构建构建命令、映射与许可
FAQ

常见问题速查

我想……去哪
手机怎么做出来快速开始 → 怎么拿到手机
换壁纸 / 改字色设置 → 更换壁纸
手机太小看不清设置 → 界面大小
给某个 App 绑一个键设置 → 每个 App 一个快捷键
放自己的音乐音乐 → 支持的格式
MP3 放不出来音乐 → MP3 只认 MPEG-1
聊天发图太占硬盘服主须知 → 图片占多少硬盘
备份要备哪些目录服主须知 → 数据存在哪儿
用资源包换手机贴图换肤(资源包)
商店里少了某个 App应用商店 → 联动 App 页
GETTING STARTED

快速开始

装什么、怎么合成、怎么开机、主屏怎么摆。

DEPS

可选依赖

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」页回答。

CRAFT

怎么拿到手机

工作台合成,一次一部:

工作台配方GRID
玻璃 玻璃 玻璃
红染料 黄染料 蓝染料
铁锭 红石 铁锭

开机与关机

操作结果
手持右键开机
H手机在背包或饰品槽里也能开机,不必先切到手上
Esc / 点机身外面关机
屏幕底部导航栏 ◁退回上一层

Esc 不分层级:开到哪一页按下去都是直接关机;要退一层走 ◁。

按键一览

模组注册的五个键,均可在原版「选项 → 按键设置 → MCphone」中改:

默认作用
key.mcphone.open_phoneH开机
key.mcphone.camera_shutterV相机:拍照
key.mcphone.camera_exitX相机:退出
key.mcphone.hud_toggleG唤出 / 收起副手 HUD
key.mcphone.hud_interact左 Alt唤出 / 收起 HUD 上那部手机的鼠标操作

五个默认键在 Minecraft 1.21.1 原版中均未被占用。此外每个 App 还能各绑一个自己的快捷键,见每个 App 一个快捷键

HOME

主屏

拖动排序

按住图标直接拖动即可换位,其余图标实时让位,松手落定。

起拖阈值移动 3 像素 才算拖动,轻点仍是打开 App
移动方式插入,不是对调——把第一个拖到第三格,中间那些依次前移
顺序存放客户端 config/mcphone/installed/<存档>.json,按存档记
新装的 App落在最后一格,不打乱已有排列

分页

每行4
每页行数最多 5 行(上限;实际行数按屏幕剩余高度计算)
页码点直径 3 像素,间距 4;只有一页时不绘制

只有一页时画一个孤零零的点,会让人以为还能往旁边划。翻页三种方式:

方式参数
滚轮
在空白处横向滑动位移超过 24 像素 触发
点击页码点直接跳转

翻页动画 160 毫秒。

把 App 挪到别的页

拖着图标停在屏幕左右边缘:边条(宽 10 像素)由浅到深亮起,停满 0.4 秒 自动翻页,手不必松开。

不是「碰到就翻」:拖向最右一列的路上必然扫过右边条,一碰就翻的话最后一格永远放不进去。

PHONE HUD

副手 HUD

手机拿在副手里时自动挂到画面上,一直亮着,停在哪一页就显示哪一页——边走边看聊天、边挖矿边看任务书,不必反复开机。

1.10.0 起。全部为客户端设置,存于 config/mcphone-client.toml
SHOW

挂出来

情形行为
手机在副手自动挂上
手机在 Curios 饰品栏、背包或主手G 手动叫出,再按一次收起
按下 F1(原版隐藏 HUD)跟着一起隐藏

G 顶掉自动那条规矩,而不是与之并列:手机在副手上时按 G 也能收起,否则会出现「按了没反应」。副手上的手机被拿走或换掉后,自动那条重新生效。

USE

操作它

Alt 唤出鼠标,再按一次收起 ——是切换,不是按住。唤出后可以翻页、点 App、在输入框里打字。

情形行为
背包或聊天框正开着时按 Alt不抢。玩家正在那儿操作
手机全屏打开时按 Alt无效果。此时已经在正经操作手机
操作中点了机身外、或被别的界面顶掉退出操作态,手机退回 HUD 继续挂着
从全屏退出(ESC、点机身外)自动挪回 HUD 那个角上

挂着的和 Alt 里的是同一部手机,不是两块屏:在 Alt 里翻到第三页,收起之后 HUD 上还是第三页。唤出鼠标期间是原版界面状态,人不会走动。

PLACE

摆位置和大小

「设置 → 副手 HUD」里可开关、调大小与位置;也可以就地摆——拖边框或状态栏挪位置,滚轮缩放

范围默认
大小40%–150%60%
锚点九个角(上/中/下 × 左/中/右)右下
偏移±4096 像素,横竖各一0

位置按锚点 + 偏移记录,换分辨率、改 GUI 缩放之后仍停在原处。

HUD 的大小与「设置 → 界面大小」是两个独立的数:一个管挂在画面上那部,一个管全屏打开那部。

KEYS

按键一览

默认作用
key.mcphone.hud_interact左 Alt唤出 / 收起鼠标操作
key.mcphone.hud_toggleG手动挂出 / 收起 HUD

均可在原版「选项 → 按键设置 → MCphone」中改键。

CONFIG

配置项

config/mcphone-client.toml

默认说明
hudEnabledtrue总开关
hudAnchorBOTTOM_RIGHT贴哪个角,九选一
hudOffsetX / hudOffsetY0 / 0从锚点再往里挪多少像素
hudScale60百分比,40–150
BUILT-IN APPS

内建 App 一览

手机里预置的十五个 App,各自去哪一页看。

LIST

十五个 App

App说明
相机取景框 + 分帧截图,照片存进 screenshots/
相册缩略图网格、大图查看、删除
音乐放你自己的音乐;唱片仓能外放给周围人听
时钟游戏内时间与还有多久天黑,下界也准
天气现在什么天,以及这种天适合做什么
应用商店安装 / 卸载 App
设置壁纸、字体颜色、设备命名、App 管理器
美西螈手机上的聊天 App:好友、会话、发图与表情、传送到好友身边
记事本随手记,能印成书
末影箱随身开末影箱。付费,1 × 末影箱
传送石去任何已激活的传送点。需 Waystones付费,1 × 传送石
浏览器在手机里上网。需 MCEF
阅读整合包里的教程书全在这儿。联动 Patchouli 等
任务书开机点一下就是任务书。联动 FTB Quests
终端打开你自己的存储终端。联动 AE2 / RS / Tom's,免费
APP · PHOTOS

拍照与相册

相机负责取景与截图,相册负责浏览与删除。两者共用游戏目录下的 screenshots/。

CAMERA

相机

取景框 + 分帧截图。照片存进游戏目录的 screenshots/,与原版 F2 截的图放在一起。

默认作用
key.mcphone.camera_shutterV拍照
key.mcphone.camera_exitX退出相机

均可在原版「选项 → 按键设置 → MCphone」中改键。

快门闪光:白闪或模糊

在「设置 → App 管理器 → 相机」中,「快门闪光」一行点击即在两者间切换。

模式表现配置值
白闪(默认)整个屏幕瞬间提到全白cameraSoftFlash = false
模糊画面糊一下再收回,亮度不变cameraSoftFlash = true

闪光持续 220 毫秒。两种都不会进照片 ——闪光在抓图完成之后才开始。

模糊借用原版菜单背景那条后处理链,不额外附带着色器文件;半径随时间收敛,因此是化开又收回,而非硬切一块糊画面。

白闪是照相机的惯例,但在夜间或暗处拍照时会把屏幕顶到全白,眼睛需要缓几秒。模糊同样交代了「拍下来了」,而不改变亮度。

设置项为客户端级,存于 config/mcphone-client.toml

GALLERY

相册

目录游戏目录下的 screenshots/
网格每行 3 张,分页浏览
缩略图长边 96 像素,按需生成并缓存
大图预览长边 512 像素
翻页底部 ◁ ▷
大图内切换左右方向
删除二次确认:第一次点击后按钮变为「再点一次确认」

右上角「打开文件夹」用系统文件管理器直接打开截图目录。

相册也是美西螈发图时的图片来源;聊天里收到的图可以存进相册,存的是原始文件。

APP · MUSIC

音乐

播放 config/mcphone/music/ 下玩家自己的音乐文件,另有唱片仓可对周围人外放。播放 / 暂停 / 继续、上一首下一首、三种循环模式、进度条、音量。

TWO PATHS

两条独立的路

曲库(耳机)唱片仓(外放)
内容来源仅 config/mcphone/music/ 下的文件,游戏内唱片不混入手持唱片点击放入,仓位 1 格
谁听得见只有本人周围玩家都听得见,声音跟随移动
由谁解码本地客户端服务端播放原版音效
可用操作播放 / 暂停 / 继续 / 切换 / 进度 / 音量仅播放与停止

外放没有暂停:原版音效系统中不存在「从中间接着放」。两条路可同时发声。

本地 MP3 / OGG 无法外放:服务端无法把玩家硬盘上的文件发给其他人,其他人的客户端上也没有该文件。
NETMUSIC

唱片仓与 NetMusic

装了 NetMusic 后,唱片仓同时接收它刻录的 CD:用其电脑方块搜歌、刻成 CD,放入手机后按播放,走到哪儿放到哪儿。

网络音乐能外放而本地文件不能,原因在于服务端只需广播一个地址,每个客户端各自去拉取。搜歌、登录、会员、刻盘均属 NetMusic;本模组只接手「播放」这一件。

FORMATS

支持的格式

放入 config/mcphone/music/ 即可,进入音乐 App 会自动重扫,无须重启游戏。

解码器扩展名说明
原版 Vorbis.ogg .oga游戏本体自带,最稳
JavaMP3.mp3 .mp2 .mp1MIT 许可,全文见 jar 内的 THIRD-PARTY.txt
内置 PCM.wav .wave .aiff .aif .au

MP3 只支持 MPEG-1

采样率是否可播
MPEG-144100 / 48000 / 32000 Hz是(绝大多数音乐属于此档)
MPEG-2 / MPEG-2.522050 / 24000 / 16000 / 11025 / 12000 / 8000 Hz

这是所打包解码库的限制,对应其 issue #8

不可播放的文件不会静默失败:曲库中该行置灰,悬停提示「MPEG-2 放不了,只认 MPEG-1」,日志中记录完整规格:

latest.logLOG
[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

设置的存放

循环模式与音量存于 config/mcphone-client.toml

(列表循环 / 单曲循环 / 随机)(范围 0–100)
默认
musicModeLIST_LOOP
musicVolume100

音量为该 App 自身的音量,最终输出还要乘以游戏主音量与唱片音量。

APP · CLOCK / WEATHER

时钟与天气

两个预装的小 App:游戏里几点了,现在什么天。

CLOCK

时钟

显示游戏内时间与现实时间。

原版时钟只有一个转盘,看得出大概是白天还是晚上,看不出「几点」,也看不出「还有多久天黑」——而后者才是玩家真正在问的问题。

它在下界与末地照常工作。原版时钟在下界会乱转(那是刻意的设计),但游戏时间在下界照常同步,因此这里显示的是地表的真实时间。

换算基准

常量
一整天24000 tick
tick 0 对应早上 6:00(偏移 6 小时)
每现实秒20 tick
日落开始tick 12000
入夜tick 13000
日出开始tick 23000

换算逻辑集中在 feature/clock/WorldClock,该类不接触 Minecraft,可单独跑断言。时间算错既不会崩溃也不会报错,只会让所有人的时钟差 6 小时。这类错误必须在进游戏之前就拦住。

WEATHER

天气

显示当前天气,以及这种天适合做什么

「现在下不下雨」抬头就看得见;有意义的是后半句:下雨适合钓鱼(雨天上钩快),打雷是抓高压苦力怕的时候,下雪可以拿铲子收雪堆雪傀儡。这些都是原版真实存在的机制。

六种天

判定结果含义图标
CLEARweather/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 · CHAT

美西螈(聊天

手机上的聊天 App,名字就叫这个。双向好友、会话列表(未读数、在线状态、最后一条预览)、气泡式会话界面。

消息存进世界存档,对方离线也能发,其下次上线即可看到。
LIMITS

上限一览

好友100 人
每人待处理申请50 条
每对会话保留消息最近 100 条
单条消息长度256 字
每对会话保留图片最近 20 张(按内容去重后计)
单张图片体积由服主定,默认 512 KB,见服务端配置
图片长边512 像素
动图帧数6–36 帧
FRIENDS

好友

加好友需对方同意。在右上角的「+」中从当前在线玩家里点选,对方在同一界面同意或拒绝;两人同时点「添加」直接成为好友。

规则说明
聊天范围仅限好友之间,陌生人发不进来
解除好友聊天记录不删除,重新加回后历史即恢复
在线状态刷新每 3 秒一次
MEDIA

发送图片与表情

输入栏左侧的「+」中有两项:图片表情

图片

即相册内容:相机 App 拍的、按 F2 截的都在其中,选中点击即发送。

也可直接把图片文件拖入游戏窗口 ——会话正开着时拖入即为发送给对方,无须先把文件移进 screenshots/ 再从相册里找。

表情

存放于 config/mcphone/stickers/,跟随客户端,换服务器、换存档均保留。导入方式两种:

  • 把图片拖入游戏窗口(表情页开着时为「收进来」而非「发出去」,可一次拖入整包)
  • 点该页右上角「打开文件夹」,用系统文件管理器放入
同一张表情反复发送,在服务器上只占一份存储:图片按内容存储(文件名是「会话键 + 图片字节」的哈希),也只占那 20 个名额中的一个。

压缩

发送前客户端先行压缩:

步骤规则
1长边压到 512 像素
2仍超出体积上限时,依次降档重压:384 → 320 → 256 → 192
3仍压不下则提示更换图片

雨天、树叶一类满屏噪点的截图 PNG 压不动,是走到第 2 步的主要情形。

其他限制
上传超时15 秒
发送冷却2 秒
同时排队最多 4 张
分块传输每块 16 KB

不存原图:手机放大看图时也只有 448×680 个真实像素,存原图只是占用服主硬盘与所有人的带宽。

动图

GIF 表情发出后在对方手机上是动的:拆成帧、拼成一张雪碧图传输,播放在客户端完成。

约束
帧数6–36 帧
雪碧图长边最大 1024 像素
超出上限时先降一档帧尺寸;再不行隔帧抽稀;仍塞不下则退回发送第一帧

表情页格子中显示的是第一帧——该页不做动画。

接收与保存

收到的图片直接显示,不套气泡

操作结果
点击图片放大铺满屏幕
放大后点右上角存进相册,存的是原始文件,存完即可再转发
点击别处收起

动图存进相册的是第一帧 ——相册是给截图用的,动图放进去别处都读不了。

图片过期

每对会话只保留最近 20 张不同的图。更早的图片消息仍留在记录中,显示为「已过期」。

保留该行而不删除:整条删去会使聊天记录凭空缺失若干句,且缺失的内容无从追溯。文字消息不受此限制,仍为每对会话 100 条。

服主可整个关闭图片功能,见服务端配置

TP

传送到好友身边

在线好友那一行的右下角有一个 7×7 的图标,点击即传送,手机同时自动关闭。

行为
落点与原版 /tp 玩家 玩家 一致:对方所站坐标,朝向也一致
跨维度支持
是否需对方同意否。好友本身已是双方同意的结果
对方是否知情是。两端各响一次末影人传送声,其动作栏写明是谁来了
费用与冷却
离线好友不显示该图标

鼠标悬停时图标点亮,该行第二行的消息预览临时替换为「传送到他身边」。

对方恰在点击瞬间下线时提示「对方已经不在线了」——在线状态 3 秒刷新一次,这几秒的空档撞得上。图标放在右下角,是为了不与未读数、时间挤占位置,名字也不必让出宽度。

服主可关闭allowFriendTeleport = false,图标不再显示,服务端也拒绝传送请求。好友关系与聊天不受影响。
APP · TOOLS

小工具:记事本 · 末影箱 · 传送石

三个各自一句话说得完的 App。

OVERVIEW

三个 App

App价格前置
记事本免费
末影箱1 × 末影箱
传送石1 × 传送石(waystones:warp_stoneWaystones

价格由内建的报价来源 feature/store/BuiltinAppPrices 声明,机制见IAppPriceProvider

NOTES

记事本

随手记录文字。

说明
存放玩家数据(playerdata/),死亡不掉落
输出写完可印成一本书交给别人
ENDERCHEST

末影箱

随身打开自己的末影箱。就是原版那一个,与方块末影箱、跨维度完全互通。

付费 App,售价 1 × 末影箱。
WARPSTONE

传送石

打开传送石碑的选点界面,前往任意一个已激活的传送点。

需要 Waystones付费 App,售价 1 × 传送石(物品 waystones:warp_stone)。

该价格仅在注册表中确实存在该物品时才登记——Waystones 缺失时这个 App 本身就不会出现。

APP · BROWSER

浏览器

在手机里上网。需要 MCEF。

PANEL

占屏面板

它是唯一一个跳出机身的 App:手机屏幕为 120×200 像素,网页在其中一行正文放不下十个字,因此点开后是一块占屏幕九成的居中面板,退出即回到手机。

面板上方一条高 22 像素的工具条:后退、前进、刷新、地址栏,右侧圆点为加载指示。工具条位于面板之外,浮在面板上方的留白里,不占用网页高度。

ADDRESS

地址栏兼搜索框

输入内容按以下顺序判定:

顺序条件行为
1://原样打开
2含空格作为搜索词
3.https:// 后打开
4其余作为搜索词
SHORTCUTS

快捷键

MCEF 自带的快捷键可用(macOS 上以 代替 Ctrl):

作用
Ctrl + 滚轮,或 Ctrl + = / - / 0缩放
Alt + ← / →后退 / 前进
Ctrl + R刷新
FIRST RUN

第一次使用需等待下载

装上 MCEF 之后首次使用还需等它下载约 200 MB 的原生库,期间显示「MCEF 还没准备好」,属正常现象。
LIMITS

已知限制

target="_blank" 的链接点击无反应,搜索结果中这类链接很常见。

成因在 MCEF:它没有注册 CefLifeSpanHandler,而离屏渲染下也没有窗口可开,于是点击不产生任何效果。变通办法:把地址复制进地址栏手动打开。

浏览器后端本身是一个可替换的扩展点,开发者侧见浏览器后端

APP · READER

阅读

把整合包里的教程手册全列在一页里。联动 Patchouli、GuideME、沉浸工程。

整合包里的教程书通常有几十本,每一本都是一个物品:想查点什么就得先在仓库里翻出那本书,翻完还得放回去;出门在外想起要看,书多半不在身上。列表中一行是书名,第二行是它出自哪个模组(玩家找书的思路几乎总是「某某模组那本书」,而不是书名)。行首小图即该书自身的物品图标。

SHELF

书架与书城

底部两个页签:

页签内容
书城整合包里有的全部书,几十本
书架玩家自己收下的那几本。打开 App 默认停在这里

收藏靠每行右端的 :点一下进书架,再点一下移出。不用长按、也不另开菜单——长按在这种小屏上没有任何提示,等于把功能藏起来。

说明
收藏存放config/mcphone/reader/shelf.json
作用域全局一份,不分存档
换整合包后当前认不出的书不显示,但不从文件中删除;换回去仍在架上
停留页签一局之内记住——从书城点开一本书,看完回来仍在书城

按存档分的结果会是「同一个包、换个服务器书架就空了」:书来自客户端装的模组,同一个整合包连到哪个服务器都是那几本。

SEARCH

搜索

两个页签顶部都是搜索框,进入该页时它已持有焦点,直接打字即可。可搜三个字段:书名、模组名、模组 id。中文客户端里书名是中文、出处是英文很常见;只搜一个字段就会出现「我明明记得这个模组叫什么,却搜不到它的书」。

排序规则

结果按分数从高到低排列:

命中方式分数
书名完全相同100
书名前缀80
书名包含60
模组名前缀45
模组名包含35
模组 id 包含20
书名命中一律排在模组名命中之前 ——搜「新生」的人要的是那本书,不是「出自某个名字里带新生的模组」的一堆书。

多个词

用空格分开,是的关系,各词可以命中不同字段(例:ars 笔记)。任一词未命中,整本即不算命中;整本的分数取最弱那一项的分。右上角的数字在搜索时变为「命中 / 总数」。

按键

这一页吃掉所有按键(含背包键),与记事本、设备命名几页相同:打拼音必然按到 e,不吃掉就成了开背包。ESC 照旧直接关机。尚未实现拼音搜索(xsmy → 新生魔艺),那需要一张拼音表。

DELEGATE

翻书仍由原模组完成

点开一本书后,接管屏幕的是它自己的界面,进度、已读标记、条目锁定全部对得上,与在背包里右键该书一致。

本模组只提供目录:Patchouli 的书界面是 272×180,手机屏幕只有 120×200,放不下;而自行重画就要把它几十种页面类型重新实现一遍,它一改版即断。

合上书后按 ESC 回到游戏;下次开机手机直接停在书架上。
SOURCES

三种书源

来路收录方式典型例子
Patchouli扫描绝大多数教程书
GuideME自动发现AE2、现代化工艺
白名单写死一份名单沉浸工程《工程师手册》

GuideME:一次全收

AE2 的作者把自家手册系统拆成了独立模组 GuideME,AE2现代化工艺都硬依赖它。GuideME 自身即具备「列出全部手册」的能力,因此这一支是自动发现——不写死任何一本书的 id,将来第三、第四个模组接上它,书城里自动就有。

点开走的是它手册物品右键那条路,因此在手机里点开与在背包里右键进入的是同一个界面、同一段历史。图标是它为每本手册创建的那个物品。

白名单:非 Patchouli 的手册

有的模组自写了一整套手册系统,与 Patchouli 无关,扫描扫不出来——沉浸工程的《工程师手册》是第一个。这类书走一份写死的白名单,每条记录它出自哪个模组、用哪个物品作图标与书名、以及如何打开。打开走的是它自己物品右键那条路ManualHelper.getManual().getGui()),连「停在上次看的那一页」都一致。书名取自该物品的名称,与背包、JEI 中显示的是同一行字。

新增一个模组:写一个 ExternalBook 实现,向 ExternalBookSource 的白名单加一行。对方未安装时自动跳过。

无对应物品的书

有些模组的书是 no_book 的,游戏内没有对应物品,只能用命令打开。它们在这里同样列出并可打开,图标使用默认的书。

QUIRKS

书籍特例

有些模组把自己的手册整个换掉了,Patchouli 中只剩一本无人维护的旧书。这类由「书籍特例」层处理,它能做三件事:把某本书从架上移除、改写其显示、接管「点开之后打开什么」。

目前只有一条——新生魔艺(Ars Nouveau):它 5.x 起换成了自研文档系统,右键笔记本打开的是那个界面,但 jar 中仍留有一本 Patchouli 书(条目只有英文,内容停在换系统那天)。因此在手机里点它的笔记本,打开的是它自己的文档界面

新增一条:写一个 BookQuirk 实现,加入 BookQuirks 的名单。对方未安装时自动跳过。

特例与白名单的分别:特例管的是「这本书扫出来了,但要特殊对待」;白名单管的是「这本书根本扫不出来,得主动报上去」。特例层刻意没有「凭空多出一本书」的能力,正是为了把这两件事分开。

这类代码一律走反射、不加编译依赖:为一个方法引入几十 MB 的前置不值,且断了要能退回默认行为(打开那本遗留的 Patchouli 书),不能变成一个点了没反应的死按钮。

APP · QUESTS

任务

开机点一下就是整合包的任务书。联动 FTB Quests,预装且免费。

IDEA

为什么是一格

任务书是一局里翻得最勤的东西——每做完一件事就要回去看下一步。而它是个物品:要么长期占着快捷栏一格,要么每次都得回箱子里翻出来。这一格把它变成「开机 → 点一下」,书仍在原处,只是不必随身携带。

FTB Quests 缺失时,本 App 不出现在主屏与商店的普通列表中,但商店的「联动App」页照常列出它并写明缺哪个模组。

FTB DRAWS

界面仍由 FTB 绘制

点开后接管屏幕的是它自己的界面,章节、进度、领奖、编辑权限全部对得上,与在背包里右键那本书一致——所调用的正是那本书右键时执行的那一句。

打不开时,说话的也是它。任务档案尚未从服务端同步、服主在配置中关闭了任务书界面、队伍被锁——这三种情形 FTB 自身都有聊天提示或 toast,本模组不在前面加一道自己的检查。

加了的话玩家点下去只会得到静默无反应;交给它,玩家看到的和右键实体书时是同一句话。

CLIENT

纯客户端

任务档案本身即同步在客户端,开界面也在客户端,服务端不会多收一个包

接触面只有一个零参方法,走反射、不加编译依赖,理由与阅读的几支书源相同,写在 feature/quests/client/FtbQuestsBook.java 的类注释里。

FREE

为什么预装且免费

任务书在绝大多数整合包里是开局白送、丢了还能再做的,卖它没有对应物;而这个 App 存在的意义就是少走几步,埋进商店等玩家自己发现,等于第一步就多走了。

WHY NOT READER

为什么不收进「阅读」的书城

教程书任务书
数量几十本全局一本
使用方式想查点什么才去翻随时要看
因此需要一个带搜索的列表一个直达的入口

收进书城的话每次都要走「开机 → 阅读 → 从几十本里找到它 → 点开」,比拿实体书还慢——多那一层列表,这个 App 就没有存在的必要了。

APP · TERMINAL

终端

在手机上点一下,打开你自己的存储终端。装了哪家存储模组,开出来的就是哪家的界面。免费。三家有一家就够。

WHICH

认得哪些终端

模组modid认得的东西
Applied Energistics 2appliedenergistics2无线终端、无线合成终端、无线样板访问终端
└ 加装 AE2WTLibae2wtlib它那些终端也全认,包括通用终端
Refined Storage 2refinedstorage无线网格、无线自动合成监视器、便携网格
Tom's Simple Storagetoms_storage高级无线终端(基础那把开不了,见下)

装了哪几家,App 详情页的简介末尾会写出来。

SLOT

终端卡槽

手机内置一个终端卡槽。点开「终端」时:

情形行为
卡槽里装了终端直接开那一台
卡槽空着打开卡槽界面,格子与背包相邻,拖入即装好
按住 Shift 点图标一律打开卡槽(用于换一台或取出)

它解决的是「身上带着好几台,到底开哪一台」:答案从「背包顺序里第一台」变成玩家自己选的那一台

卡槽里的终端死亡不掉落,与壁纸、笔记、唱片仓中的唱片一样跟随玩家。

卡槽是加成,不是替代。终端可以一直留在背包里,卡槽页上的「打开终端」照样能开它。

这一条是刻意的:终端一旦收进手机就不在背包里,各家自己的快捷键(AE2WTLib 的补货/磁铁/收纳、RS 的打开无线网格)便找不到它。想用那些快捷键的人不装卡槽即可,代价只是多按一下。

TOMS

Tom's 的基础无线终端开不了

这不是缺陷。那把终端的设计是「瞄准范围内的终端方块右键」,本身不存在「隔空打开」这一能力。

因此它放不进卡槽;背包里带着它点「终端」会跳过它并说明原因——比点了没反应要好。要隔空打开,用高级那把。

BOUNDARY

边界:这一格不定任何规则

范围、耗电、绑定哪个网络、界面长什么样,一条都不由本模组决定。这一格只做一件事:挑出要打开的那一台,然后按它自己的方式打开。
因此
进度你在 AE2 / RS / Tom's 里的进度即为你在这一格里的进度——增幅卡、绑定网络、跨维度全按对方规则
打不开时说话的也是它们(如 AE2 的 Device is not linked.),本模组不在前面加自己的检查

加了自己的检查,两套规则迟早对不上,而对不上的表现是「手机上说不行、手里拿着又能开」。

FREE

为什么免费

末影箱那一格卖一个末影箱、传送石那一格卖一个传送石,因为 App 本身就是那件实物的替代品。这一格替代不了任何东西——它开的是你自己那台终端,没有终端它一格内容都没有。真正的门槛是那台终端,再收一次等于收两遍。

APP · STORE

应用商店

安装与卸载 App。安装状态持久化,系统 App 置灰且不可卸载。

STORE

安装与卸载

说明
安装状态存放客户端 config/mcphone/installed/<存档>.json,与主屏排列同一份
系统 App卸载键置灰并写明原因,例如「设置」
付费 App详情页显示价格,买不起时按钮置灰。见小工具
卸载后仍可从商店重新安装

App 也可以由附属模组自带的来源提供,见IAppSource

COMPANION

联动 App 页

商店最后一格是「联动App」,排在所有可下载 App 之后。点进去一行一个列出:图标、名字、缺哪个模组、已装还是未装。未满足的整行压暗,看得见、点不动

它回答的是「我怎么没有这个 App」——依赖缺失的 App 不出现在主屏与商店普通列表中,否则玩家会点到一个必然报错的图标;但完全不出现,玩家也就无从知道自己缺了什么。

声明方式是否收录
requiredMods()
companionMods()是(该 App 当前不可用时)
当前可用的 App否——它没有被任何模组卡住,列出只是噪声

手机内「设置 → 关于」的联动模组列表则一律列出这些模组,并标明装没装。开发者侧的声明方式见 companionMods()

SETTINGS

设置

App 管理器、快捷键、壁纸、界面大小、字体颜色、设备名称、副手 HUD。

INDEX

这一页做什么

这一页干什么存在哪
App 管理器每个 App 一页:来历、前置/联动、卸载、快捷键
每个 App 一个快捷键按一个键直接开机进某个 Appmcphone-client.toml
更换壁纸选一张 PNG 当壁纸服务端,按玩家
界面大小把整个手机放大缩小mcphone-client.toml
字体颜色六个预设mcphone-client.toml
设备名称给自己这部手机起名服务端
副手 HUD手机挂在画面上:开关、大小、位置mcphone-client.toml
关于版本、联动模组装没装
MANAGER

App 管理器

列出已安装的每一个 App。点一行进入它自己的一页:图标、名字、作者、版本、介绍,以及它由哪个模组提供、所声明的前置与联动模组装没装。

卸载在这一页,且需点击两次(第一次上膛、第二次才真卸),与相册删照片同一条规矩。系统 App 的卸载键置灰并写明原因。卸载后仍可从应用商店装回。

这一页往后是每个 App 的开关都该待的地方——操作区是一行一个的形状,加一个开关就是加一行。第一个这样的开关是快捷键,第二个是相机的快门闪光

HOTKEY

每个 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、或被原版/别的模组的键位占用时,该行变为「再按一次强制」并写明占用者;再按同一组合即强制绑定,此后该行常驻一个感叹号警示。

这套快捷键不出现在原版的「按键设置」界面里。让「按 E 同时开背包和手机」这种事默默发生,日后查不出是谁干的。加修饰键并不等于避开冲突:原版自身的键位在判定是否响应时不看修饰键,因此 Ctrl+E 照样会连带开背包。真按下时手机会把该键位这一下攒的点击丢弃,不至于等关掉手机再补开一次背包。

手机已经打开时按这些键不会切换 App——那时玩家多半正在手机里打字。

WALLPAPER

更换壁纸

把任意尺寸的 PNG 放进 config/mcphone/wallpapers/,在这一页中选择。右上角「打开文件夹」用系统文件管理器直接打开该目录;点过之后这一页会盯着目录,拖进去的图切回游戏即可看见,无须退出重进。

壁纸选择由服务端保存并同步,多人游戏中每位玩家的壁纸各自独立。
SCALE

界面大小

把整个手机放大或缩小:拖动滑条,或点 − + 每档 25%。

范围75% – 300%
默认100%
配置键uiScale

调整时手机(连同这一页本身)实时跟着变,无须另外预览。

为什么不用原版的 GUI 缩放:那一项是全局的,为了看清手机把它调大,聊天框、物品栏、所有界面都会跟着变大。这一项只管手机。放大是整体缩放而非重排布局:手机内所有元素的相对位置不变,鼠标坐标由模组自行换算。

贴合清晰倍数(默认开启)

配置键 uiScaleSnap。字体是位图、按最近邻绘制,因此每个像素是否方正只取决于一个乘积:GUI 缩放 × 界面大小。是整数则横平竖直;不是整数则有的列占两像素、有的占三像素,细线时粗时细。开启时档位自动对齐到 1 / GUI缩放

GUI 缩放档位
2100 / 150 / 200 …
3100 / 133 / 166 …
4100 / 125 / 150 …

− + 一次正好走一档。关闭后可拖到任意值,代价是上述不匀。

窗口放不下时自动夹回:配置里的数不变,窗口拉大后又回到设定值;被夹住期间这一页会显示一行「窗口放不下,实际 xxx%」。
FONT

字体颜色

六个预设,配置键 fontColor

预设
白色(默认)WHITE
黑色BLACK
琥珀AMBER
天青CYAN
薄荷MINT
樱粉PINK

用了浅色壁纸就换黑色,否则浅底浅字看不清。选中即生效,无须重启。

只有画在壁纸上的字跟着变。导航栏、通知、聊天气泡、商店按钮上的字不跟随:那些底是手机自带的深色构件,字跟着一起变就陷进底里看不见了。
NAME

设备名称

给自己这部手机起名。起过名的手机在物品栏显示该名。

长度上限24 字符,服务端做校验与截断
铁砧改名优先级更高,会覆盖设备名

手机屏幕只有 120 像素宽,24 字已远超能显示的量。

HUD

副手 HUD

手机拿在副手里时自动挂到画面上。开关、大小与位置在这一页,也可以就地摆——拖边框或状态栏挪位置,滚轮缩放。完整说明见 副手 HUD

CLIENT

客户端配置一览

以上标注「客户端」的设置全部存于 config/mcphone-client.toml,跟随这台电脑,换服务器、换存档均保留。也可从模组列表的「配置」按钮进入修改。

默认对应界面
fontColorWHITE字体颜色
musicModeLIST_LOOP音乐的循环模式
musicVolume100音乐 App 音量,0–100
appHotkeys每个 App 一个快捷键,一条写成 <App id>=<键>
cameraSoftFlashfalse快门闪光true 为模糊
uiScale100界面大小,75–300
uiScaleSnaptrue贴合清晰倍数
hudEnabledtrue副手 HUD 总开关
hudAnchorBOTTOM_RIGHTHUD 贴哪个角,九选一
hudOffsetX / hudOffsetY0 / 0HUD 从锚点再往里挪多少像素
hudScale60HUD 上那部手机多大,40–150

服务端那一份(serverconfig/mcphone-server.toml)见服主须知 → 服务端配置

IME

关于中文输入

手机里的输入框使用的就是原版按 T 那个聊天框的同一个控件,能否输入中文与原版完全一致。

需要注意的是 Minecraft 不会把输入法候选窗定位到光标处(GLFW 缺少相应接口,原版聊天框亦然),打拼音时基本看不见候选列表,等于盲打。稳妥的做法是用 Ctrl+V 粘贴。
SKINNING

换肤(资源包

界面上的每个视觉元素都可以用贴图替换。放了贴图就用贴图,没放就用内置配色,功能完全不受影响——不会出现缺图变紫黑格的情况。

把 PNG 放进资源包的 assets/mcphone/textures/,路径按下面几张表(相对该目录)。改完按 F3+T 重载资源包即可看到效果,无须重启游戏。下表共 44 个贴图位,与代码中的 PhoneSkin.Element 枚举一一对应。

PHONE

phone/ —— 机身、导航、主屏

文件名画的是什么建议尺寸没放时
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

chat/ —— 美西螈

文件名画的是什么建议尺寸没放时
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

music/ —— 音乐

文件名画的是什么建议尺寸没放时
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/STORE

gallery/ · store/ —— 相册与商店

文件名画的是什么建议尺寸没放时
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

browser/ —— 浏览器

文件名画的是什么建议尺寸没放时
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

reader/ —— 阅读

文件名画的是什么建议尺寸没放时
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纯色(另一页不画底)
SLIDER

settings/ —— 滑条

文件名画的是什么建议尺寸没放时
settings/slider_track.png滑条的槽(「界面大小」那一条,将来的音量条也用它)100×6,整张横向拉伸纯色
settings/slider_fill.png滑条已填充的那一段100×6纯色
settings/slider_knob.png滑块4×14(比槽高,压在槽上)纯色
settings/step_button.png滑条两端 − + 键的底14×14纯色,悬停时提亮

这四个位刻意使用通用名称:此后其他滑条(如音量)复用同一组贴图,只需绘制一次。

ICONS

App 图标与天气图标

App 图标单独放在 app/ 下,文件名就是 App 的短名,都是 20×20:

app/ICONS
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.pngrain.pngsnow.pngthunder.pngdry.pngnone.png,按当前是什么天各显其一。

这六张与上面几张表性质不同:它们没有兜底色,模组自带全套,资源包只做覆盖。建议六张一并替换,只换其中一两张会出现画风不统一。玩家头像不在换肤范围内:那是玩家自己的皮肤,取自 Tab 玩家列表。
SCALE

尺寸与拉伸

尺寸不必精确匹配,贴图会被拉伸到目标区域。要不变形,按建议尺寸或其等比放大绘制即可。

「建议尺寸」指的是画在屏幕上有多大,不是文件必须多大。整数倍放大的图同样可用,且线条细、有弧度的图案这样更清楚。

模组自带图的倍数:

贴图倍数
phone/frame.png2 倍(272×432)
导航栏三个键10 倍(400×140)
书架两颗星、浏览器三个键、滑块与 − + 键4 倍

放大倍数取整数 ——非整数倍在低 GUI 缩放下会掉线条。

NINE-PATCH

九宫格:.png.mcmeta

默认整张拉伸。不带元数据的贴图会被拉到实际大小,带圆角的图在宽气泡上抻长、在窄气泡上压扁,因此纯色或纵向渐变最稳妥。

要让圆角不变形,在 PNG 旁放一份同名的 .png.mcmeta

phone/bubble_self.png.mcmetaJSON
{"mcphone_skin": {"border": 3}}

该图即改走九宫格:四角按源图上那 3 个像素原样绘制,只有四条边和中间被拉伸,气泡无论多宽多高,四个角都一样大。

情形行为
border 大于源图短边的一半忽略,退回整张拉伸
老资源包未写该文件行为不变,无须改动

自带图中使用九宫格的有五张:

贴图border
chat/bubble_self chat/bubble_peer chat/input_bar3
settings/slider_track settings/slider_fill2(使两端圆头不随条一起抻长)
STATE

状态只能靠亮度或透明度

贴图改不了颜色。悬停、点不动这类状态,在没有贴图的年代是换一个颜色画出来的;一旦这个位上有了贴图,那个颜色就再也看不见。

因此模组在有贴图时改用别的表达:

状态表达
导航键、商店按钮悬停整张按倍数提亮(资源包画的是什么颜色,亮起来还是那个颜色)
浏览器三个键点不动整张压暗
音乐键、聊天的「+」把高亮块画在贴图底下

绘制贴图时不必自行表达这些状态。半透明有效:alpha 介于 0 与 255 之间的像素会正常混合,抗锯齿边缘、整块半透明的底(「壁纸上压一层暗色」这类效果)都可以画。1.7.40 之前这类像素会被当作不透明绘制,边缘发硬、半透明的底变成实心。那是模组的缺陷,不是贴图的问题。

OTHER

其他

44 个贴图位模组现已全部自带,一个不缺。资源包放同路径的图仍照常覆盖;「没放时」那一列的兜底色平时轮不到出场,保留在表里是因为资源包塞进一张坏 PNG 时仍会退到那一档,而不是变成紫黑格。
情形行为
文件不是合法 PNG退回纯色并在日志记一条警告,不崩溃

生成完整清单(含每个位现有的图长什么样、缺图时退回哪个颜色):

终端SHELL
python3 docs/make_texture_manifest.py

输出一张 HTML。不在文档里存一份静态清单,是因为那种清单必然过期——PhoneTheme 头部那一份就是这么失效的。

老路径仍然识别。1.2.7 之前所有贴图平铺在 textures/gui/ 下(phone_frame.pngapp_icon_chat.png 这类)。按那套做的资源包无须改动即可继续使用——新路径找不到时自动回退,并在日志中提示一句。新做的资源包请按上面几张表。
SERVER ADMIN

服主须知

服务端配置、图片占多少硬盘、备份要备哪些目录。

CONFIG

服务端配置

文件位置:

环境路径
单人游戏存档目录下的 serverconfig/mcphone-server.toml每个存档一份
专用服务器serverconfig/mcphone-server.toml
改完重进世界(或重启服务器)生效。
默认范围说明
allowFriendTeleporttrue关闭后好友那一行的传送图标不再显示,服务端也拒绝传送请求。好友关系与聊天不受影响
allowChatImagestrue关闭后输入栏左侧的图片键不再显示,服务端也拒收上传。已发送的图不删除,仍然可见 ——关掉的是「再发新的」
chatImageMaxKb51264–768单张图片的 KB 上限。客户端按它压缩,服务端按它接收
这三项必须是服务端配置:客户端配置在每个玩家自己电脑上,用它来管「能不能传送」等于把规则交给被管的人。服务端配置由服主一份说了算,NeoForge 会在玩家连入时同步给客户端,因此界面能据此提前隐藏按钮,而真正的拦截仍在服务端——界面只是不给入口,伪造客户端照样发得出包。
DISK

图片占多少硬盘

最坏情况 = chatImageMaxKb × 20 × 好友对数。

每对会话最多保留 20 张不同的图,所以每对好友封顶是该值的 20 倍。默认 512 KB 即每对 10 MB;200 对常聊的好友是 2 GB 上限。

实际远小于上限:Minecraft 的截图大片是天空与地形,长边压到 512 之后常常只有几 KB。上限卡的是满屏噪点的画面(雨天、粒子)以及动图——动图是所有帧拼成一张,因此最吃这个上限。

图片按内容存储(文件名为「会话键 + 图片字节」的哈希),因此同一张表情在同一对好友之间发一百次,硬盘上仍然只有一份,也只占那 20 个名额中的一个。

想怎么调结果
调小 chatImageMaxKb客户端自动降一档尺寸重压,图变糊;动图先掉帧、再不行退成一张静态图。功能不坏,画质让路。128 KB 大致是每对 2.5 MB
调大图更清楚、动图更流畅,硬盘与带宽跟着涨
完全关闭allowChatImages = false,已发送的仍然可见

上限 768 KB 是硬的:再往上就顶到原版「服务端发给客户端」那 1 MB 的包上限。服务器启动时会扫描一遍图片仓,删除没有任何消息认领的文件(写完文件、消息尚未落盘就崩溃会留下这种孤儿)。

DATA

数据存在哪儿

做备份时要知道:

数据位置随什么走
好友关系、聊天记录世界存档的 data/存档
聊天里发的图片世界存档的 mcphone/chat-images/存档
笔记、已购 App、唱片仓、终端卡槽、壁纸选择、未读标记玩家数据(playerdata/存档 + 玩家
App 安装状态与主屏排列客户端 config/mcphone/installed/<存档>.json玩家自己的客户端
书架收藏客户端 config/mcphone/reader/shelf.json玩家自己的客户端,全局一份不分存档
表情包客户端 config/mcphone/stickers/玩家自己的客户端,全局一份不分存档
客户端设置(字色、界面大小、快捷键、HUD)客户端 config/mcphone-client.toml玩家自己的客户端
前三行在服务器上,删存档即丢失;后四行在玩家自己电脑上,服务器备份不包含它们
ID

装了 Integrated Dynamics 的话

MCphone 会在方块注册的末尾,把 Integrated Dynamics 全部方块的掉落表提前解析一次。

不改变任何掉落行为,也不给玩家多出任何功能——它的作用是让别人的崩溃能被正确指认。

成因:NeoForge 的注册阶段只要收到任何一个模组抛出的异常,就会把整个注册表回滚成原版状态;而回滚过程本身会在 Integrated Dynamics 的墙上火把处踩到一个空指针,于是真正的错误被顶掉,崩溃报告上只剩一句

crash reportTEXT
Trying to access unbound value: integrateddynamics:menril_torch_stone

看起来像是 Integrated Dynamics 的问题,实际上它只是最后一个倒下的。提前解析一次即可绕开那个空指针,使回滚正常完成并抛出真正的原因。

这不会救回服务器:起不来还是起不来,它只让崩溃报告指认对人。

细节写在 compat/IntegratedDynamicsCompat.java 的类注释里。未安装该模组时,这段代码一行都不会执行。

ADDONS INDEX

附属一览

由第三方开发、基于 MCphone 的 App SPI 构建的模组。本页只做索引。

01已收录 02收录规则 03写一个附属
Minecraft 像素风格的悬浮手机插画 第三方模组索引 · 独立发布
这些不是 MCphone 的一部分。它们各自独立发布、各自维护、各自授权,出了问题请到其所属仓库反馈,不要开在 MCphone 的 Issues 里。装与不装、装哪些,由玩家和服主自行判断。
LIST

已收录

附属作者做什么许可
mcphone-deepseek november521 手机里的 DeepSeek 对话 App,纯客户端 MIT
mcphone-deepseek第一个基于 App SPI 的附属,用到 IPhoneApp + IPhonePage + PhoneCanvas + PhoneStyle,独立仓库、结构完整,可作为新附属的起手样板 —— 附属接口文档也指向它。
RULES

收录规则

开一个 Issue 说明下列各项即可,或直接改本页提 PR:

字段要求
仓库地址公开可访问
做什么一句话,说清它在手机里加了什么
许可仓库里要有 LICENSE
适配范围支持哪些 Minecraft 版本与加载器
收录只表示「它存在、能装」,不表示 MCphone 审阅过其代码或为其行为背书。一个附属的实现类跑在玩家的客户端上,能做的事与任何模组无异。

长期无法构建、或仓库已删除的条目会从本页移除。

BUILD

写一个附属

三分钟做一个 App 开始,四步:加依赖、实现 IPhoneApp、登记 SPI、放语言文件与贴图。

动手之前先看一眼 非踩不可的坑 —— 其中「客户端类型泄漏到服务端」与「SPI 构造失败是静默的」两条,症状都不指向出问题的 App。
FOR DEVELOPERS

附属接口文档

给附属模组作者:做一个手机 App,画进屏幕,给它定价。

本文档分几页,按你需要的深度挑着看:

TABLE

一张总表

所有接口都在 dev.november.mcphone.api 及其子包,命名以 I 开头。

类型所在包用途
IPhoneAppdev.november.mcphone.api.app画在手机屏幕上的那个 App,主入口
IPhonePagedev.november.mcphone.api.appApp 内部的一个界面(主屏 → 列表 → 详情)
IAppSourcedev.november.mcphone.api.store把一组 App 报告给商店与主屏
AppInfodev.november.mcphone.api.store一个 App 的描述(id、名字、图标、简介、价格)
AppEntrydev.november.mcphone.api.storeAppInfo + 创建 IPhoneApp 的工厂
ICostdev.november.mcphone.api.cost价格家族的根接口
Cost.Free…cost.builtin免费
Cost.Item…cost.builtin用物品付
Cost.Items…cost.builtin用一组物品付
ICostRegistry…cost注册自己的价格类型
IAppPriceProvider…cost把定价来源整个换掉
PhoneApidev.november.mcphone.api取单例、判版本、判某 App 在不在
PhoneTextures…api.ui手机内部的共享绘制原语
ScrollSpec…api.ui滚动列表怎么滚(快慢、回弹、边距)
INotifyHandler…api.notify通知 API
ApiVersion…apiAPI 版本号,用于版本与两端安全

下文按包逐一说明。每节都给最小代码;照抄即可跑,不必先读完整份文档。

SPI

SPI 注册

MCphone 通过 Java 的 ServiceLoader 发现附属:你的 jar 里要有一个 META-INF/services/dev.november.mcphone.api.store.IAppSource 文件,里面一行是实现类的全名。

META-INF/services/dev.november.mcphone.api.store.IAppSourceSPI
com.example.mymod.ExampleAppSource

MCphone 启动时会加载所有这样的实现,调用 apps(),把返回的 AppEntry 合并进主屏与商店。不需要事件、不需要注解处理器,一个文本文件搞定。

EXAMPLE

有没有现成的例子

有。november521/mcphone-deepseek 是一个真实发布的附属,代码不长,结构就是「一个 IAppSource、一个 AppInfo、一个 IPhoneApp、一张列表页 + 一个详情页」。读它比从头猜快。

更短的起步模板见五分钟做一个 App

ADDON QUICKSTART

三分钟:做一个
能点开的 App

四步:加依赖、实现 IPhoneApp、登记 SPI、放语言文件与贴图。

01依赖 02实现 IPhoneApp 03SPI 注册 04语言文件与贴图
Minecraft 像素风格的悬浮手机插画 MCphone 1.10 · Minecraft 1.21.1
STEP 01

第一步:依赖Dependencies

发版时逐个目标构建,全部 jar 挂载于同一个 Release,文件名形如 mcphone-<版本>-<目标>.jar。取与自身目标一致的一个 —— 当前仅 mcphone-<版本>-1.21.1-neoforge.jar 一项可用。

下文以 NeoForge 为例,构建插件为 ModDevGradle,而非 ForgeGradle:不存在 fg.deobf 一类写法。

当前无公开 maven 仓库,将 jar 置于项目的 libs/ 下即可:
build.gradleGROOVY
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 中声明依赖,使加载顺序正确:

neoforge.mods.tomlTOML
[[dependencies.yourmod]]
modId = "mcphone"
type = "required"          # 可选依赖写 "optional"
versionRange = "[1.2.13,)" # 用到 IPhonePage 即为这一版起;只做图标可写 [1.0.46,)
ordering = "AFTER"         # 必须 AFTER:要用的注册表得先就位
side = "BOTH"
当前值
Minecraft1.21.1
NeoForge21.1.200+
MCphone1.10.0
STEP 02

第二步:实现 IPhoneApp

CalculatorApp.javaJAVA
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()

不要继承内建的 PhoneApp 基类:它把命名空间写死为 mcphone,且位于 core 包,不属于 API。直接实现接口。
实现类只能在客户端加载,见两端安全
STEP 03

第三步:SPI 注册

META-INF/services/com.november.mcphone.api.client.app.IPhoneAppSPI
文件:src/main/resources/META-INF/services/com.november.mcphone.api.client.app.IPhoneApp
内容:com.yourmod.client.CalculatorApp

文件名就是接口全名,内容是实现类全名,一行一个。

STEP 04

第四步:语言文件与贴图

assets/mymod/lang/zh_cn.jsonJSON
{
    "mymod.app.calculator": "计算器"
}

英文另开 en_us.json,两边的键要对齐。

assets/mymod/textures/app/calculator.pngTEXTURE
20×20、PNG-32。路径要与 getIconTexture() 返回的一致
贴图缺失也能运行:原版会绘制紫黑格,不会崩溃,但玩家看得见。
IPHONEAPP

IPhoneApp / 方法一览

一个 App 是怎么被手机调起来、画出来、关下去的。

CONTRACT

它是什么

dev.november.mcphone.api.app.IPhoneApp 是一个 App 的运行态——玩家在主屏上点了你那个图标之后,手机实例化的那个对象。它不是 App 的描述(描述是 AppInfo),描述不变,运行态每次开机都是新的。

它管管不管由谁管
名字、简介、图标、价格、id不管AppInfo / AppEntry
装没装、出不出现在主屏不管IAppSource 与商店
App 当前画哪一页currentPage(),见IPhonePage
开机拉数据、关机写盘onOpened() / onClosed()
屏幕上画出来什么render()
LIFECYCLE

生命周期

时机回调做什么
玩家点主屏图标onOpened(player)拉数据、开第一页、初始化滚动位置
每帧render(pose, mx, my, pt)画当前页
玩家点机身外 / 按 ESConClosed()写盘、丢引用、不在这里关连接(已随世界卸载断开)

关 App 与换世界、登出的顺序是:先 onClosed(),再丢引用、再卸载世界。网络连接会随世界卸载自动断开,不要onClosed() 里再关一次——那种「先关连接、再清缓存」的顺序会让卸载流程等连接,或在空引用上再关一次。

OPENPAGE

openPage() 与空状态

进 App 后,你有多少数据,就开多少页

你的情况开机后应该开哪一页
本来就有东西(已配置、有历史)直接开那条数据的详情页
什么都没有(首次安装、刚配置完)开列表页,并把空状态画在列表页上

不要「空数据时开一个假的空状态页」:那个假页没有返回栈、没有可点的东西,玩家只能走导航栏 ◁,而且连进 App 时看到的第一屏和正常用完全不一样。空状态应该画在正常会用到的那一页上——列表为空、详情加载中——那一页照样能翻、能点新建、能返回,只是内容区是一段提示。

RENDER

render():坐标基准

手机屏幕内部是 120 × 200 的坐标空间(外框另算)。所有 render() 的坐标都在这个空间里:

区域坐标
顶部状态栏y = 0 … 10
正文区y = 12 … 186
底部导航栏y = 188 … 200
正文宽度x = 0 … 120

正文区域之外是手机自带的状态栏与导航栏,别往里画——你的内容会被它们盖住,或者反过来盖住它们。

WIDGETS

复用原生控件

手机里那些按钮、滑条、滚动条不是每个 App 各自画的,而是一套共享控件,放在 dev.november.mcphone.api.ui.PhoneTextures 与相关的 …ui 类里:

画什么
PhoneTextures按钮底(正常 / 悬停 / 禁用)、滑条(槽 / 填充 / 滑块)、九宫格圆角块
ScrollSpec滚一滚有多快、有没有回弹、内容区上下留多少

用同一套控件,而不是自己画一份。否则换肤只能换一部分——资源包作者改了按钮贴图,你的 App 还是老样子,界面一半新一半旧。

换肤的完整路径表见 用户文档;从 API 这边调用它们的方式,看 mcphone 源码里相机 / 相册那两个内置 App 怎么画的,照着调即可。

WALLPAPER

壁纸与字色跟着走

玩家换了壁纸、改了字色,你的 App 不必自己跟着改

  • 壁纸由手机画在最底层,你的 render() 画在它上面
  • 画在壁纸上的字,用手机提供的文本绘制方法,它会自动取玩家选的字色
  • 导航栏、通知、聊天气泡这些自带构件不走玩家字色,别试图用自己的绘制去替换它们
COMPANION

companionMods():商店里那一页

你的 App 依赖某个别的模组(例如 Waystones、MCEF、NetMusic),实现一个 companionMods() 方法,返回那些 modid:

情况商店的行为
依赖的模组装了App 正常出现在主屏与商店列表里
依赖的模组没装主屏与商店列表里不出现,但商店的「联动App」页会把它列出来,写明缺哪个模组,整行压暗、看得见点不动

它回答的是「我怎么没有这个 App」。两个方法的区别:

方法含义
requiredMods()缺了它 App 就根本不能用,不进主屏,但联动页里列出来
companionMods()缺了它 App 仍能跑,只是少一块功能;同样进联动页

两者都收进联动页,当前可用的 App 不列——那是噪声。

OPEN_PAGE

openPage():开别的 App 的页

你的 App 想跳到另一个 App 的某个界面(例如从设置跳到壁纸选择、从聊天跳到相册),用 PhoneApi.get().openPage(...)

要点
走接口调用,不直接 new跨模组 new 对方的类会在对方卸载时崩;走 PhoneApi 的查找,找不到就返回空,你的 App 退一格
目标 App 得在对方的 App 没装 / 被禁用时,调用静默失败,不抛异常
返回栈对方页打开后按 ◁ 回到你的页,不是直接回主屏
IPHONEPAGE

IPhonePage / 屏幕上的一个界面

主屏 → 列表 → 详情,这种层级是怎么做出来的。

CONTRACT

它是什么

dev.november.mcphone.api.app.IPhonePage 是 App 内部的一个界面。App 本身是「当前在第几页」,IPhonePage 是那一页长什么样、收什么输入。

IPhoneAppIPhonePage
实例每次开机新的可以是单例(静态常量),也可以带参数 new 出来
关心当前是哪一页、整体生命周期这一屏画什么、点哪里、滚哪里
返回键不直接处理由它决定:能不能退、退到哪
NAV

返回键与导航栏 ◁

屏幕底部的 ◁ 是手机自带的,玩家按它或点它,手机会问「当前页能不能退」:

你的返回栈◁ 的行为
详情页弹栈,回到列表页
列表页(App 的根页)整个 App 关闭,回主屏
主屏不响应(主屏没有上一层)

你只管维护自己那一页的栈(一个 Deque<IPhonePage> 或两个字段足矣),不要自己在屏幕上画返回键——和底部那条自带导航会重复。

ESC 是手机级快捷键:它不分层级,按下去整个手机直接关机,不会先走 ◁。因此你的页面里不需要为「按 ESC」写处理。

OPEN

怎么开一页

在 App 内部换页,就是把 currentPage() 返回的对象换掉:

ListPage.java(节选)JAVA
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 引用干净——对方将来改名 / 拆包时,你的详情页不会跟着断。

SCROLL

列表很长:ScrollSpec

一页里有很多条,滚不完怎么办:用 ScrollSpec 描述怎么滚,把滚动位置存在你自己的字段里:

它管
一次滚多少滚轮一格的步长
回弹滑过头之后拉回多少
上下边距正文区里留多少不画内容,避免贴到状态栏 / 导航栏

滚多远只在当前实例里:换页时新的页从零开始滚。不要把滚动位置存进存档——那是会话级的状态,不是持久数据。

NOTIFY

通知:INotifyHandler

你的 App 想在屏幕右上角弹一条通知(新消息、任务完成),用 dev.november.mcphone.api.notify.INotifyHandler

要点
走单例,不 new通过 PhoneApi 取当前处理器
不存图、不存字通知是一帧的事,弹完就丢;要持久化的内容请放进 App 自己的页
点了通知由你决定跳哪一页;手机只负责弹与消失

聊天 App 的未读条数与「你被传送了」那种系统提示共用同一个槽位,别刷屏——同一事件连发十条,玩家只会觉得手机坏了。

IAPPSOURCE / APPINFO

IAppSource · AppInfo · AppEntry

一个 App 的静态描述:id、名字、图标、简介、价格,以及把它交给手机的注册来源。

SPLIT

为什么分成两个

「一个 App 是什么」和「一个 App 现在跑成什么样」是两件事:

AppInfo(描述)IPhoneApp(运行态)
生命周期整个游戏都在,只有一份每次开机新建,关机即丢
内容id、名字、图标、简介、价格、作者当前页、滚动位置、拉到的数据
谁创建附属模组在启动时写死手机在玩家点图标时 new

商店列表、主屏图标、「设置 → App 管理器」的详情,全都来自 AppInfo;点进去之后才轮到 IPhoneApp。

APPINFO

AppInfo

ExampleAppInfo.javaJAVA
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短,主屏一格放不下长句
icon20×20 PNG,没有兜底,路径错就是空白格
summary商店详情里那一行,说明这 App 干什么
costICost 家族
APPENTRY

AppEntry:描述 + 工厂

光有描述还不够——手机还得知道「点下去 new 哪个类」。AppEntry 就是把 AppInfo 和一个创建运行态的工厂绑在一起:

ExampleAppEntry.javaJAVA
public class ExampleAppEntry implements AppEntry {
    @Override
    public AppInfo info() { return ExampleAppInfo.INFO; }

    @Override
    public IPhoneApp create() {
        return new ExampleApp();          // 每次开机新实例
    }
}
SOURCE

IAppSource:你有哪些 App

一个附属可能有多个 App(例如「小工具」下挂着记事本、末影箱、传送石三个)。IAppSource 就是「我这一个附属有哪些 App」的清单:

ExampleAppSource.javaJAVA
public class ExampleAppSource implements IAppSource {
    @Override
    public List<AppEntry> apps() {
        return List.of(
            new ExampleAppEntry(),
            new AnotherAppEntry()
        );
    }
}

这个类同时是 SPI 文件里那一行的指向:

META-INF/services/dev.november.mcphone.api.store.IAppSourceSPI
com.example.yourmod.ExampleAppSource
旧版 SPI 注册的是 IPhoneApp。当前版本注册的是 IAppSource一个来源报一组 App,商店与主屏从这里合并。
PRIORITY

顺序与去重

规则行为
新装的 App 排在哪主屏最后一格,不打乱已有排列
两个附属报了同一个 id先加载的赢,后加载的记一条日志,不崩
你的 App 在商店里排第几按附属加载顺序,不保证固定;别在介绍里写「排在第 N 个」
ICOST FAMILY

ICost 家族

App 卖多少钱:免费、用物品、用一组物品,以及把整套定价换掉。

WHY

为什么要付费 App

有的 App 替代了一件原版实物:末影箱那格卖一个末影箱,传送石那格卖一个传送石。手机本身没花钱就能用,但这种 App 一装上,那台实物你以后永远用不上了——收一份对应的物品,等于把原来的成本还给玩家。

反过来,终端那格免费:它开的是玩家自己那台终端,没有终端它一格内容都没有。真正的门槛是那台终端,再收一次等于收两遍。

BUILTIN

三个内置价格

怎么写商店里长什么样
Cost.Freenew Cost.Free()「免费」
Cost.Itemnew Cost.Item(Items.DIAMOND, 1)一颗钻石
Cost.Itemsnew Cost.Items(Map.of(Items.DIAMOND, 2, Items.EMERALD, 1))两件并排

买不起时商店按钮置灰并写明缺什么。购买走原版消耗:扣物品、放进去、写进存档,全程服务端判定,玩家不能伪造客户端绕过。

「价格」是 App 的属性,不是手机的属性。同一部手机上有的 App 免费、有的要钻石,这是正常的。
REGISTRY

注册自己的价格类型

你想要「用经验等级付」「用服务器积分付」这种 MCphone 没想过的价格,实现 ICost,再通过 ICostRegistry 注册:

ExampleLevelCost.javaJAVA
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 级经验」这一行字即可。判定与扣除都在服务端,客户端只负责显示与提示。

PROVIDER

换掉整个定价来源

有的整合包有自己的经济系统,不想让玩家用物品付。IAppPriceProvider 就是把「价格从哪来」整个换掉:

职责
App 自己声明的 cost默认来源,写在 AppInfo 里
IAppPriceProvider覆盖者:在它眼里这个 App 值多少、怎么付

内建的 feature/store/BuiltinAppPrices 就是默认来源,声明了末影箱、传送石那几个价格。整合包想全部免费,写一个 Provider 把所有 App 都报成 Cost.Free 即可,不必改 MCphone、也不必改每个附属

COMPAT

版本与两端安全

API 怎么保证不悄悄崩,以及服务端 / 客户端的边界在哪。

PROMISE

兼容承诺

API 的版本号由 ApiVersion 给出,按下面的规矩演化:

变更类型对已发布的附属
加新方法(带默认实现)、加新包、加新类不破坏,附属照跑
改方法签名、删类、改语义会升大版本,旧的标记 @Deprecated 保留一个版本再删
渲染坐标、颜色常量属实现细节,可能变;别在自己代码里写死 120×200 之外的数

mcphone 与附属的版本对不上时,游戏启动时会在日志里给出明确提示(哪个附属要求哪个 API 版本、当前 mcphone 是哪个),而不是进游戏点开某个 App 才崩。

SERVER

哪些事只能在服务端做

动作必须在
扣物品、判付费成功服务端。客户端发个包「我付了」会被服务端再验一次
写玩家数据、写存档服务端
传送玩家、改游戏规则服务端
画屏幕、读鼠标、动画客户端。服务端没有屏幕
查「玩家现在开着哪个 App」各自知道各自的,跨端要发包

规则很简单:凡是影响游戏世界的,都得在服务端判定;凡是只画给眼睛看的,都在客户端。中间要传的信息用 NeoForge 的网络包,别图省事在客户端直接改玩家血量——那只改了你自己屏幕上的数,一帧就被服务端盖回去。

HEADLESS

纯服务端也能装

MCphone 可以装在没有客户端的专用服务端上:玩家数据、好友、聊天、付费判定照常工作,只是没有人能真的打开屏幕。

因此你的 App 代码不能在类加载时就碰渲染相关的东西(GL 状态、字体渲染器、屏幕宽高)。要画的东西放在 render() 里,那个方法只在客户端被调到;类定义、静态字段、AppInfo 的构造里都不要 new 字体、不要拿 Minecraft.getInstance().font。

否则纯服务端一开服就 NoClassDefFoundError。

BROWSER BACKEND

浏览器后端

浏览器那块大面板底下用什么加载网页,是一个可替换的扩展点。

DEFAULT

默认实现:MCEF

MCphone 内置的浏览器用 MCEF 加载网页。它在 MC 生态里是最成熟的选择,但有两个已知限制,写在 用户文档 里:

  • 首次使用要下载约 200 MB 原生库
  • target="_blank" 的链接点了没反应

这两条都来自 MCEF 本身,MCphone 改不了。

EXTEND

替换成别的引擎

将来你想用别的浏览器内核(比如另一个 CEF 封装、或者一个离线文档服务器),实现浏览器后端的扩展点:

你要实现做什么
创建 / 销毁后端打开面板时起,关闭时收
加载一个 URL地址栏敲完、回车
绘制到面板给一块纹理,手机把它贴上去
转发鼠标 / 键盘点击、滚动、输入

手机只认「一块纹理 + 一组输入事件」,不关心底下是 CEF 还是什么。后端选哪个由运行环境决定,不要在自己的附属里硬编码依赖 MCEF——玩家可能换了后端。

STATE

状态归谁管

状态归谁
历史记录、书签、缓存后端自己
面板大小、工具条位置手机
当前 URL、加载中圆点后端报给手机,手机画在工具条上

后端不必知道手机的坐标系;它只要把一张固定尺寸的纹理画好,手机负责把它贴到面板上、跟着界面大小缩放。

PITFALLS

非踩不可的

做附属最常犯的五个错,每个都给了现场和修法。

01

图标路径写错,主屏空白一格

现象:App 出现了,但图标是空白;日志里一条「texture missing」。

icon() 返回的 ResourceLocationassets/<命名空间>/textures/… 之下的相对路径。最常见的错是写成 "icon/example.png" 而文件放在 assets/yourmodid/textures/gui/icon/example.png——命名空间对,路径少了一层。用原版的 getResourceLocation 检查一下,或直接看 mcphone 内置 App 的图标是怎么放的。

02

坐标画到正文区之外

现象:你的按钮看着在屏幕里,点不到;或者画到了顶部状态栏 / 底部导航栏上。

正文区只有 y = 12 … 186。按钮放在 y = 190 就压在导航栏上了;放在 y = 5 就顶到状态栏。滚列表时用 ScrollSpec 留的上下边距,别自己硬算。

03

在客户端改游戏世界

现象:你在客户端把玩家传走了,自己看着动了一下,下一帧又弹回来;或者血量改了,一帧就复原。

凡是影响游戏世界的,都得发网络包到服务端判定。客户端改的只是本地副本,服务端不承认。完整边界见版本与两端安全

04

类加载时碰渲染,纯服务端开服崩

现象:单人游戏能跑,专用服务端一开服就 NoClassDefFoundError,栈顶在你的 App 类。

你的 AppInfo 是静态常量,它的构造在类加载时就跑。别在那里 new 字体、拿 Minecraft.getInstance().font、读屏幕宽高。要画的东西放在 render()。见纯服务端也能装

05

硬编码内置 App 的 id 或坐标

现象:MCphone 升了一个小版本,你的 App 突然找不到「那个内置页」了,或者按钮偏了几像素。

不要写 "mcphone:camera" 这种 id 去找内置 App——要用就走 openPage() 的查找,找不到就静默退一格。坐标、颜色常量同理:用 PhoneTextures,不要自己画一份按钮底。

BUILD FROM SOURCE

源码构建

不需要分叉仓库。本模组以 MIT 许可发布,克隆后用 Gradle 打包即可。

CLONE

克隆与构建

终端SHELL
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 一项。

MAPS

映射

用官方映射(MojMap)。跑客户端用 ./gradlew runClient,跑服务端用 ./gradlew runServer

LICENSE

许可

本模组以 MIT 许可发布——随便改、随便发,保留版权声明即可。第三方下载与发布渠道见首页