Skip to content

平板 + 本地 txt 小说:合到最新 main 上 - #3

Draft
november521 wants to merge 10 commits into
mainfrom
feature/tablet-reader
Draft

平板 + 本地 txt 小说:合到最新 main 上#3
november521 wants to merge 10 commits into
mainfrom
feature/tablet-reader

Conversation

@november521

Copy link
Copy Markdown
Owner

平板与本地 txt 小说这两样做完之后一直躺在 feature/tablet-reader 上,从没进过 main,
也没进过任何一个发布包 —— v1.10.1 是从 main 切的,游戏里当然找不到它们。

分支从 0f9e0a8(v1.10.1-beta.1)分出去,此后 main 走了 31 个提交把仓库改成多目标结构。
这个 PR 先把 main 合进分支(cb1c5e7),再把落点归位(0045499)。

带进来的功能

  • 平板 —— 第二台设备(DeviceKind/DeviceMetrics/NavBarLayout,配方、模型、贴图)
  • 阅读 App 的本地 txt —— 书架能拖着排、看本地 txt 小说、全屏、边走边看、拖进来就收
  • 相机坐标水印(照片右下角印坐标,坐标同时写进文件名)
  • 一个 HUD 修复:收起 HUD 那部会把手里那部的屏幕一起报灭

合并本身

没有一处内容冲突。 已有文件的改动由 git 的改名识别落到新路径上;17 处冲突全是
「新文件该放哪一层」的落点冲突。归位那一个提交处理了 verifySharedIsTargetNeutral
抓出的两处:ReaderKeyHandler 整个搬去 platforms/1.21.1-neoforge/PhoneScreenOnSync
留在 shared/,只把「又过了一 tick」那层加载器差异挪回平台,这边收成不带事件参数的 tick()
接缝清单按 updateSeamsDoc 重写。

⚠️ 这个 PR 要 CI 回答的问题:1.20.1-forge 会不会红

1.20.1-forge 在 main 上目前是绿的,而这批改动至少有两处会碰到它,尚未处理

  1. platforms/1.20.1-forge/.../PhoneScreen.java 还在调 PhoneScreenOnSync.turnedOn/turnedOff
    —— 这条分支把那套边沿触发的入口换成了每 tick 轮询,那三个方法已经没了
  2. TxtBookSource 是 ★ 被共用代码引用的接缝,眼下只有 1.21.1-neoforge 那一份

另有一类查不出来的:新进 shared/ 的 9 个文件(TxtReaderPage 那 530 行绘制尤其)
会被 1.20.1 一起编,而四条判据挡不住签名漂移 —— 只有真编一遍才知道。

所以先开 PR 不直接推 main:让 CI 把两个目标各编一遍,红在哪儿看清楚了再决定
1.20.1 这一侧怎么补(它是另一条线正在做的目标,动它之前要先说一声)。

🤖 Generated with Claude Code

november521 and others added 10 commits September 8, 2026 14:10
手机外面包一圈铁锭合出来。屏幕从竖着的 120×200 变成横着的 240×168,
宽正好是手机的两倍,长宽比跟着物品模型走。

它不是另一部手机:App、聊天记录、笔记、壁纸、买过的东西、卡槽里的终端
全是同一份——那些数据本来就存在玩家身上,不在手机那件物品上。所以两台
可以同时带着,随手拿哪个都行。

屏幕尺寸从两个编译期常量变成跟着设备走的 DeviceMetrics。各页早就是按传
进来的宽高排版的,所以真正要改的只有"谁来给这两个数"这一层。主屏与商店
的列数改为按内容区宽度算、整排居中;手机上算出来仍是 4 列、左边空 8,
与从前写死的两个数一模一样。

导航栏在平板上立到右边(NavBarLayout):横过来之后最缺的是高度,168 的
屏幕底下再切 14,页面能用的比手机还矮;而宽有 240,右边让出 14 几乎看不
出来。条摆在哪儿、每个键占哪一段只此一处算,画与判命中取的是同一份。

侧键跟着机身转到上下两条边(电源在上、音量在下,都靠左那一头——右侧那条
是导航栏,实体键再挤过去,能按的和不能按的就全堆在同一侧了)。界面里那
一圈外壳也有了平板自己的一张 frame_tablet.png,不再是拿手机那张顶着用;
资源包没做这张时照旧退回手机那张切九宫格。

机身贴图换成雾蓝灰,去掉了背板噪点;屏幕亮着的那一档改为满亮度渲染
(NeoForge 的 neoforge_data),暗处也看得清。

装了 Curios 的话平板也能放进那个手机槽,顺带给这个槽补上了名字——它此前
显示的是 curios.identifier.phone 这行原始键,中英文都没有。

docs/DeviceLayoutTest 90 条断言钉住这些算术,第一条守的就是"平板别把手机
的排布改了"。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
玩家身上可以同时开着两台——副手 HUD 挂一部、手里全屏开一部。这时候把
HUD 那部收起来(按 G、在设置里关掉副手 HUD、手机离开副手),"现在亮的是
哪一部"算出来是"全灭":手里那台正开着界面的机器,在别人眼里屏幕黑了。

漏的是"另一头"。原来那套是边沿触发的:开机报一次、关机报一次、挪位再报
一次,于是每一处让设备生或死的地方都得记得报告——全屏开、HUD 挂上、ESC、
点机身外、被别的界面顶掉、手机离开副手、按 G 收起。漏掉任何一处都不报错,
只是留下一台"界面开着、屏幕黑着"的机器。

而"这会儿亮的是哪一台"本来就是个纯查询:全屏那台(mc.screen)、HUD 那台,
谁拿在手上算谁。所以改成每 tick 算一次,谁都不必记得报告,也就没有"漏报"
这回事。代价是最多晚一 tick,发包仍然只在答案变了的时候。

顺带消掉一个坑:原来那个 exclude 参数存在的唯一理由是"原版 setScreen 先调
removed() 再换 mc.screen,所以关的时候 mc.screen 还是它自己"。现在关机那一
刻根本不问,下一 tick 它已经不在了——这条注释连同它描述的陷阱一起没了。

PhoneScreen 从此不再引用 PhoneScreenOnSync。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
一、书架自定义排序

按住一本上下拖,其余的当场让位,松手就是新顺序。拖的时候画的是排完之后
的样子,不必让玩家自己在脑子里算"松手会插到哪儿";拖到列表上下边缘外面
会自动滚,书多过一屏也能把最后一本拖到第一位。

只在书架页、且没在搜的时候能拖:书城的顺序是"这个整合包里有什么",排它
没有意义;而搜索结果是筛过的几本,"把第 3 本拖到第 1 位"在完整的架子上是
哪一位,没有一个答案是玩家预期的。

要紧的是那些当前认不出的书(换过整合包的存档里会有):它们不显示、不参与
排序,但必须留在原处。所以排序不是"给可见的排完再拼回去",而是可见的那些
只在自己占的那几个位置里重排(ShelfOrder,36 条断言)。

二、本地 txt 阅读器

把 txt 放进 config/mcphone/reader/books/,它就出现在书城里,点开就是手机里
的阅读器。这条书源不依赖任何模组,于是阅读 App 事实上恒定可用——本地阅读
本身就是这个 App 的功能,不该因为没装 Patchouli 就消失。

编码自己猜(TextDecoder):先看 BOM,再严格试一遍 UTF-8(多字节序列自校验,
GBK 中文几乎不可能整段蒙混过关),都不是就把 GB18030 与 Big5 各解一遍、挑
"解出来更像中文"的那个。猜错的代价是满屏乱码,没有偷懒的余地。

章节自己认(TxtChapters):规则抄的是 legado 自带的那份 TXT 目录规则,它被
真实小说验证得最多。最值钱的是那几个否定环视——第二部(?!分)、第一节(?!课)、
正文(?!完|结)、第三集(?![合和]),没有它们,正文里的句子会被当成标题把一段书
劈开。keyword 那一支比 legado 更紧一档:后面必须隔一个分隔符,否则"正文一。"
这种正文首句也会中招。认不出章节的书按两千字一段切开。

一部几百万字的小说也翻得动,因为排版只对当前这一章做:整本折行要好几秒还得
一直攥着,一章两三千字则一帧之内算完。只在卷首放一个"第一卷"的书会再切开。

读到哪儿按文件名记(TxtProgress),换服务器换存档都跟着走——书是玩家自己的
东西,不属于任何一个存档。目录盖在正文上,返回键先收目录再回书架;翻页有点
屏幕左右两半、滚轮、方向键三条路。

三样新东西都是纯算术、都有断言钉着(36 + 75 + 26 条):排序差一格、目录多两
条、编码猜错整本乱码,这三种错在游戏里都看不出来。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
上一版能看 txt 了,但读起来的那一层不够:手机屏幕一页两百来字,一章要翻
十来次;而且手机一旦成了打开的界面,原版就不吃移动键,只能站着看。

一、全屏

右上角多一个「全屏」。它不是另开一个 Screen,而是同一页收到的矩形变成了
整个窗口——TxtReaderPage 从来就是照给进来的宽高排的,所以排版代码一行没
改,这也正是当初把宽高做成参数而不是常量的原因。

一页从两百来字变成一千多字。正文不铺满窗口而是居中一栏(320px,约 35 字
一行):三十到四十个字是读起来最省力的长度,六十多个字一行时,眼睛每次
回到行首都要找一下。

切换会换一个宽度,于是同一章重新折行、页数跟着变。按"读到这一章的百分之
几"换算回新页码,而不是留着旧的——留着的话,从全屏(一章 3 页)退回手机
(一章 12 页)会停在第 3 页,玩家眼里是往回跳了一大截。新页数要等下一帧
排完才知道,所以先记比例,由 renderPage 落成页码。

HUD 上那副面孔永远不全屏:它挂在角上就该是小的,那正是它存在的理由。

二、边走边看

两个新键(默认 PageUp / PageDown):手机挂在副手 HUD 上时直接翻页。

为什么非得绕开界面:HUD 上那副面孔不是 mc.screen,收不到任何输入;而一旦
把它变成 mc.screen,人就走不动路了。只给翻页开这一条不经过界面的路——翻页
是看书时唯一的高频操作,目录、切章、换书都可以停下来做。

三、放书的两条新路

「书城」页右上角多一个「文件夹」,交给系统的文件管理器打开小说目录(目录
不存在时先建出来,玩家点它的时候多半正是空的)。这一格换掉的是书城页的
本数——那个数看一眼就够,而「文件夹」是往里加书的唯一入口。

以及拖放:书架页开着时把 txt 拖进游戏窗口就收下了,一次一叠也行,与表情
走同一条路(PhoneScreen.onFilesDrop)。重名加 (2)(3) 而不是覆盖——覆盖掉
同名那本等于把玩家读了一半的书连同进度一起换掉。加后缀这段单拎成了
uniqueName:后缀加到 .txt 后面就不再是文本文件,扫描时直接看不见,而症状
只是"我拖进去的书不见了"。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
一、放进目录的书直接进书架,不必再去书城点 ☆

往 config/mcphone/reader/books/ 里放一本书,这个动作本身就是"我要看它"。再要求
玩家去书城找一遍、点一下星,是在为一个已经表达过的意愿再收一次费。

所以扫到的本地小说一律上架(ShelfStore.ensureShelved),加在末尾、不插队;书城
那一页也不再列它们——本地书不是"这个整合包里有什么",两处都列一遍,玩家会以为
是两本不同的书。

本地书那一行没有 ☆:它在不在架上由目录里有没有这个文件决定,画一颗点不动的星
只会让人反复去点。要它离开书架就把文件挪走——只要文件还在,下次刷新也会放回来,
这是唯一说得通的规矩。空出来的那一格让给书名,本地小说的文件名往往比手册长。

「文件夹」按钮也跟着从书城页挪到书架页:本地书现在住在书架,往里加书的入口就该
在这一页。书城那一格换回"共几本"。

二、去掉行右端那个三道杠把手

它想解决的是"怎么让人知道这一行能拖",但代价是行右端多了一个与 ☆ 挤在一起、
高度还对不齐的图案,而整行本来就能按住拖——没有它也不影响任何操作。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
拍下来的照片右下角多一行 X/Y/Z,就是按快门那一刻站的地方。默认开着,
「设置 → App 管理器 → 相机 → 坐标水印」里关。

坐标是【画进照片里】的,不是附带的一份数据:相册里、截图文件夹里那张 png、
发给没装本模组的人的那一份,看到的都是同一行字。代价是拍完不能反悔——关掉
开关也擦不掉已经拍下的那些。

这一行是相机里唯一允许入镜的一层。其余每样东西都在躲镜头:取景框在拍照那
一帧被整层跳过,白闪与模糊从抓取之后才起算,照片里只该有世界。所以它画在
CameraHandler 那个 return 【之前】——那个 return 的意思正是"这一帧要被拍下来"。
画到 return 之后的话,取景时照样看得见、照片里却没有,而这个错法在开发环境
里看不出来。

顺带定下的两件事:取景看到的就是照片上的样子(同一句代码画的);坐标戳与
取景框共用同一个留白(CameraOverlay.margin),换分辨率、改 GUI 缩放时两者
一起动。

字是带阴影的不透明白:照片的底是任意一块世界,纯白落在雪地、沙漠、夕阳上
都会糊掉,而一圈暗边在什么底上都读得出来。格式走翻译键
(mcphone.camera.stamp),资源包与整合包因此能改成自己想要的写法。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
两处都是"照片里那行坐标看不清"。

一、字号不再跟着 GUI 缩放走

照片是按物理像素抓的(Screenshot.grab 抓主渲染目标),而界面上一个字有多少
物理像素取决于玩家的 GUI 缩放。同一台 1080p:缩放 3 时字高 27 像素、占画面
2.5%,看得清;缩放 1 时只有 9 像素、0.8%,照片放到 100% 才勉强认得出。也就是
说戳有多大,取决于一个与拍照毫无关系的设置。

改成按画面高度算目标比例(2%),倍数取整之后再放大——位图字体放大到 1.7 倍
会把每个笔画糊成两像素灰边,而这一行要的恰恰是清楚。常见配置(缩放 2、3)下
与从前一模一样,只有缩放很小的那一档会被放大,那正是从前看不清的那一档。

二、描边代替阴影

阴影只往右下偏一像素。照片的底是任意一块世界,白字压在雪地、沙漠、夕阳上时,
左上那两条边仍然与底色糊在一起。改成四面各画一遍黑字再把白字压上去,什么底
上都圈得住。八次 drawString 在一个满屏渲染世界的地方可以忽略。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
烧进像素那一行是给"发出去给别人看"用的,在游戏里它读不出来:手机相册把整张
照片塞进一百来像素宽(1080p 的照片显示成 108×61),那行 27 像素高的字只剩
一两个像素。改多大的字号都救不回来——这是"看的地方太小",不是"画得太小"。

所以坐标顺手写进文件名,相册看大图时解回来、用界面文字显示在照片下面那一行。
界面文字不随照片缩放,多小的照片都清清楚楚。

为什么是文件名,不是 png 元数据、也不是另存一份索引:文件名不花任何额外代价
——原版 Screenshot.grab 本来就收一个文件名参数,而写元数据要把几 MB 的文件读
回来再写一遍,另存索引则是又多一份会与磁盘对不上的状态。附赠的好处是在文件
管理器里排一眼就看得见每张是在哪儿拍的。代价是改名就没了,这与阅读进度按文件
名记是同一个取舍。

那一行原本显示的是时间戳文件名。认不出坐标的(F2 截的图、这一版之前的照片)
照旧显示文件名,所以没有信息变少。显示用的是与烧在照片上同一个翻译键——同一张
照片,界面上和像素里是同一行字。

两处优化:重名时缀 _1 _2(指定文件名之后原版不再自己让路,同一秒在同一格连拍
会互相覆盖);相册那一行按文件名缓存解析结果,正则只在换一张照片时跑一次。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这条分支从 0f9e0a8(v1.10.1-beta.1)分出去,此后 main 走了 31 个提交,把仓库从
单一 src/ 改成了 shared/ + layers/ + platforms/ + versions/ 的多目标结构。分支上
那 8 个提交(平板、阅读的本地 txt、相机坐标水印)全落在已经不存在的路径上,先合
过来再谈进 main。

合的结果:**没有一处内容冲突**。已有文件的改动由 git 的改名识别自动落到新路径上;
17 处冲突全是「新文件该放哪一层」的落点冲突,git 按目录改名的多数派给了建议位置,
这次先照单收下 —— 落点对不对要按 shared/PLATFORM-SEAMS.md 的判据一个个核,
那是下一个提交的事,不混进合并里。

已知待办(下一个提交处理):新增的 9 个文件被放进了 shared/,而 shared/ 是三个
加载器、所有版本共用的一份,1.20.1-forge 也会编它。TxtReaderPage 这类做绘制的
是否真的版本中立,没验过就不能留在那儿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
verifySharedIsTargetNeutral 抓出两处「加载器导入」落在 shared/ 下 —— 都是上一个
合并里 git 按目录改名的多数派塞进去的,不是判据认可的位置:

  ReaderKeyHandler     整个类就是 NeoForge 的 ClientTickEvent 订阅者,
                       两轴都不中立 → platforms/1.21.1-neoforge/
  PhoneScreenOnSync    上游它本来就在 shared/,是这条分支把它从边沿触发改成
                       每 tick 轮询时带进了 ClientTickEvent

后者不能整个搬走 —— 它是 shared/ 的老住户,搬走等于把「哪台设备亮着」这段判断
复制到每个平台。要搬的只是「又过了一 tick」这个事实怎么来:那一层三个加载器各不
相同(NeoForge 的 ClientTickEvent.Post、1.20.1 Forge 的 TickEvent.ClientTickEvent
加 phase 判断),留在 platforms/ 下;shared/ 这边收成一个不带事件参数的 tick()。

接缝清单按 ./gradlew updateSeamsDoc 重写,diff 只多出这次真正新增的三条:
ReaderKeyHandler(加载器轴)、CameraStamp、TxtBookSource(★ 被共用代码引用)。

1.21.1-neoforge:build 通过,16 份断言测试全绿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@november521

Copy link
Copy Markdown
Owner Author

CI 跑完了:1.21.1-neoforge 绿,1.20.1-forge 红(run 34329579963

这个 PR 先挂着不合。 1.20.1-forge 在 main 上是绿的,不该由这条分支把它推红;
缺的那部分是这个目标的一次追平,留给正在做它的那条线,做完再合。

报了 100 errors —— 那是 javac 的默认上限,实际只会更多。全部集中在 4 个文件
而且都是同一件事的余波:平板进来之后,"屏幕多大"从写死的常量变成了一个
DeviceMetrics 参数。1.20.1 没有平板,所以每一处的答案都是 DeviceMetrics.PHONE

文件 处数
platforms/1.20.1-forge/.../core/client/PhoneScreen.java 71
platforms/1.20.1-forge/.../feature/settings/client/PhoneHudEditor.java 17
platforms/1.20.1-forge/.../feature/gallery/client/Gallery.java 5
platforms/1.20.1-forge/.../feature/chat/client/ChatMediaPicker.java 5

一、没了的常量(56 + 4 + 4 处,机械替换)

原来 现在
PhoneTheme.PHONE_WIDTH (28) DeviceMetrics.PHONE.screenW()
PhoneTheme.PHONE_HEIGHT (28) DeviceMetrics.PHONE.screenH()
PhoneTheme.PHONE_TOTAL_WIDTH (2) DeviceMetrics.PHONE.totalWidth()
PhoneTheme.PHONE_TOTAL_HEIGHT (2) DeviceMetrics.PHONE.totalHeight()
PhotoGridPainter.COLS (4) PhotoGridPainter.colsFor(可用宽度) —— 列数跟着屏幕宽走了,不再是写死的 3

二、多了个 DeviceMetrics 头参数(31 处)

PhoneHudPlacement.{effectiveScale,originX,originY,width,height,derive,place,clampIntoWindow}
PhoneScale.{fit,effective,clampedByWindow}PhoneChassis.{drawScreenBackground,drawStatusBar,drawNavBar,drawFrame}
NavBarLayout.ofPhotoGridPainter.{gridWidth,cellX,cellY}HomeGrid.render

三、三处单独的

  1. PhoneScreenOnSync.turnedOn/turnedOff/relocate 没了PhoneScreen.java:172/1378/1432)。
    这条分支把它从边沿触发改成了每 tick 轮询 —— 起因是收起 HUD 那一处漏报,手里全屏开着的
    那台会跟着一起灭。现在 shared/ 那边是一个不带事件参数的 PhoneScreenOnSync.tick()
    forge 侧照 PhoneKeyHandler 的写法挂一条 TickEvent.ClientTickEvent + phase == END 调它,
    然后把那三处调用删掉即可。
  2. PhoneItem.isDevice(ItemStack)shared/.../PhoneContainerMenu.java:161 要它)。
    forge 那份还叫 isPhone —— 平板也是设备,改名跟上。
  3. TxtBookSourceshared/.../BookSources.java:51 直接 newBookList.java:787 取它的
    SOURCE_ID)。它实现的 BookSource 是甲类接缝、各平台各一份,所以这个类也得各平台一份,
    GuideMeSourcePatchouliSource 同理。

后两条已经进了 shared/PLATFORM-SEAMS.md 的清单(TxtBookSource 带 ★)。

查不出来的那一类

新进 shared/ 的 8 个文件(TxtReaderPage 那 530 行绘制最可疑)这次没报错
说明签名漂移在它们身上没发生 —— 不过那是编到 100 个错误就停了之后的结论,
上面这批修掉之后要再看一遍。

@november521
november521 marked this pull request as draft September 9, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant