Skip to content

Added Storage Fluid Port. 添加了仓储流体端口 - #4806

Open
PigeonNian wants to merge 15 commits into
Anvil-Dev:dev/1.21/1.6from
PigeonNian:fluidtank/1.21/1.6
Open

Added Storage Fluid Port. 添加了仓储流体端口#4806
PigeonNian wants to merge 15 commits into
Anvil-Dev:dev/1.21/1.6from
PigeonNian:fluidtank/1.21/1.6

Conversation

@PigeonNian

@PigeonNian PigeonNian commented Sep 11, 2026

Copy link
Copy Markdown
Contributor
  • resolved [TODO] 仓储流体端口 #4792
  • 增加了之前缺失的部分快捷键
  • 修改了jei转移前的清空物品去向
  • 修复了会把物品放入盔甲栏的问题

Pigeon_Nian added 5 commits September 11, 2026 18:05
- 新增 StorageFluidPort 方块及对应方块实体和渲染器支持
- 仓储端口扫描逻辑扩展,支持仓储流体端口互联共用核心路径
- 客户端存储界面新增流体伪槽位,渲染流体图标和数量
- 增加流体槽位鼠标交互,支持空桶现场取液操作
- JEI 集成增强,支持识别空容器+仓储流体的现场桶装配方输入
- 增加高效流体高度偏置计算,仓储流体端口控制流体水位
- 优化 FluidTank 渲染,支持内缩像素调整避免 Z-fighting
- 增加仓储流体端口对应合成配方定义
- 修正物品分拣器默认面朝,潜行时反向摆放逻辑
- 优化FluidNetworkManager调用,避免level为空时的空指针异常
- 调整StorageJeiSupport的hasFluidFor方法逻辑,修正流体检测判定错误
- 修正TerminalJeiTransferSupport中判断空容器需求的条件,从deficit <= 0改为deficit == 0
- 调整导入引用顺序和删除无用导入,整理代码结构
- StorageScreen中修复getStorageSlot方法重复定义的问题
- StorageFluidPortBlockEntity新增获取核心主位置方法getCoreMainPos
- 细节错误调整和注释优化,提高代码健壮性和可读性
- 为 ICategory 接口增加 testFluid 默认方法,支持流体判定
- 实现 FluidCategory 流体分类,定义流体分类图标与名称
- 在 OrCategory 和 AndCategory 中覆盖 testFluid 实现复合判定逻辑
- 命名空间分类支持对流体命名空间的判定
- 注册并引导流体分类,添加流体分类语言条目
- PlayerSetting 中添加默认流体分类条目
- StorageScreen 支持流体格的左键倒液体和右键物品行为存入
- StorageClientStub 和 StorageServerStub 支持移动物品时根据鼠标键决定是否倾倒流体
- 优化流体倾倒后空容器的回收逻辑,优先入仓储再回退玩家背包
- 服务端增加流体条目排序与分类过滤,保证流体正确参与存储排序与筛选
- 完善流体相关 RPC 调用参数及处理,兼容左右键操作不同逻辑
- 丰富客户端 TexturedButton,支持右键操作回调功能
- 新增流体分类数据文件定义,确保数据层支持流体分类功能
- 在移动物品之前记录点击的物品,避免首次移走整叠后记录被清空
- 实现 Shift+双击左键批量移入同种物品,与原版保持节奏一致
- 添加 findInventorySlotWith 方法,寻找仍持有目标物品的背包槽位
- 更新 lastQuickMoved 字段,用于跟踪上一次快速移动的物品
- 调整 StorageServerStub 导入顺序,优化代码结构
- 删除无用的minY变量,直接使用inset作为Y轴起始点
- 调整流体渲染盒子的Y轴起始和结束坐标为inset和计算后的maxY
- 确保气体类型流体充满整个水箱且透明度正确

refactor(setting): 精简PlayerSetting类,移除废弃方法和不必要导入

- 删除addCustom(ItemStack)方法及相关FilterCategory导入
- 优化导入列表,去除未使用的类
- 保持自定义类别添加功能代码简洁明了
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /opt/data/workspace/AnvilCraft && git show tgt1216:src/main/resources/assets/anvilcraft/textures/font/small.png > /tmp/small_base.png && git show pr4806tmp:src/main/resources/assets/anvilcraft/text...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806

操作: opened
范围: 42 个文件(31 Java,12 新增,0 删除)/ 3748 行 diff
验证方式: 沙箱无 gradle/网络,未做本地编译;全部结论由 PR diff + 目标分支(dev/1.21/1.6 @ d442720b4)源码交叉验证得出。gh 未认证(token invalid),未自动改标题。


📋 声称验证表(对照 #4792 的 TODO 逐条核对)

#4792 要求 状态 证据
配方:空/潜影壳/空 + 储罐×3 + 空/潜影壳/空 → 1 个 ShapedRecipeLoader#storageFluidPort 与生成 json 完全一致
可连接集装箱 / 存储站,与仓储端口可互相延伸 StoragePortBlockEntity.findSoleCore + isPort() 双向识别两种端口
128 B 容积、单流体、拆除保留、门格海绵右键清除 CAPACITY_MB = 128*1000saveToDrop + getDrops + 创造模式 playerWillDestroyMENGER_SPONGE 分支
非气体流体:自动调等效高度维持 50%~75%,越远越快,上限 20 computeNextHeightBias()(0.5/0.75、RATE=8、clamp ±20);FluidNetworkScanner.heightBiasAt 接入管网
气体通过模拟压力维持 50%~75% 同一 heightBias 进入 effectiveHeight,气体分支沿用
UI 显示流体、不占类别、按数量排序、mB/B 单位、三位有效数字 ⚠️ 显示与单位格式 ✅(FluidAmountUtil);排序折算与 TODO 文字不一致,见下
点击流体格用铁桶取一桶,缺桶给提示 FLUID_BUCKETfillBucketFromStoragebucket_missing / not_enough 语言条目双侧齐备
放入桶装流体自动倒入同流体端口,无端口则存桶物品 pourIntoFluidPort + findAcceptor,失败回退 view.insert
JEI 填充合成识别「空桶 + 流体」并自动盛装 produceFilledContainer + addProducibleFluidContainers + countProducibleContainers 预检

🔴 关键

1. StorageFluidRegistry 的登记在端口改挂到另一个存储时不会解除 → 跨存储读/写他人流体

StorageFluidPortBlockEntity#validateLink()(BE 行 210~227)只在「解析不到核心」时注销,重新挂到新核心时只做 register(newId, ...)旧存储名下的 pos → dim 条目会一直留着

if (core == null || ...) { StorageFluidRegistry.unregister(this.worldPosition); return; }
...
StorageFluidRegistry.register(storage.getId(), serverLevel, this.worldPosition);  // 旧 id 未清

unregister 只在 setRemoved() 里调用,所以只要端口 BE 还活着(A 链断开后搭到 B 链,间隔 < 20 tick 就能做到)旧条目就一直在。之后:

  • StorageFluidRegistry.collect(A) 会把该端口的流体算进 A 的 UIlivePorts 只校验「该位置是个端口 BE」,不校验它当前挂在哪),同一份流体在两个存储里各显示一遍;
  • drain(A, fluid, n) 会真的把流体从 属于 B 的端口 抽走,findAcceptor(A, fluid) 也会把 A 的桶倒进 B 的端口 → 跨存储流体丢失/串账。

顺带两个同源问题:

  • key 不含维度PORTSMap<UUID, Map<BlockPos, ResourceKey<Level>>>(行 39),而既有同类注册表 StorageBlockRegistryMap<ResourceKey<Level>, Map<UUID, Set<BlockPos>>>——先按维度分层正是为了避免坐标撞车。现在 unregister(BlockPos) 会把这个坐标从所有维度的所有存储里删掉(register 也会同坐标覆盖),主世界/下界同坐标各有端口时会互相误删(20 tick 后会自愈,但期间该端口从 UI 消失)。
  • livePortsMap.copyOf(ports)(行 174)迭代,顺带在每次 sync/排序/交互时复制一遍 map。

建议: unregister 改成 (ResourceKey<Level>, BlockPos) 或在重挂前先 unregister 旧 id;注册表按 StorageBlockRegistry 的「维度优先」结构对齐,并考虑在 ServerStoppedEvent 清表。


⚠️ 警告

2. produceFilledContainer 的部分失败回滚会同时吞掉流体和空容器(StorageServerStub 行 2483~2530)

int drained = StorageFluidRegistry.drain(id, content, perUnit * count);   // 全量抽出
if (drained < perUnit * count) return 0;
int removed = 0;
while (removed < count && consumeOne(inventory, view, emptyContainer)) removed++;
if (removed < count) {
    acceptor.fill(content.copyWithAmount(perUnit * removed), EXECUTE);   // 只灌回 removed 份
    return 0;                                                            // 且 removed 个空桶没还
}

失败分支的注释写的是「保持原子性」,实际是:(count - removed) 桶的流体已被抽走且不灌回 → 流体凭空消失;已被 consumeOne 吃掉的 removed 个空容器既不还回也不计入产出 → 空桶凭空消失。另外 fill() 的返回值被忽略,端口容量不足时连 removed 份都补不回去。建议改成「按需逐份 drain + 失败即整量回滚 + 归还已消耗容器」,或先 SIMULATE 确认空容器够用再 EXECUTE

3. working / getCoreMainPos() 与类注释自相矛盾(BE 行 45~48、111、119、320)

类 Javadoc 写「整个连通组件必须恰好接触一个核心才工作」,物品版 StoragePortBlockEntity 也用 if (!this.working) return; 真的卡住转移;但流体版的 working 只有 @Getter全仓库无任何读取点getCoreMainPos() 同样是无调用者的死代码。因此一个没接到任何核心的端口照样:onLoad 注册进 FluidNetworkManager、能力照常暴露、heightBias 照常把自己当 20 格高度的源/汇抽放流体。如果这是设计意图(端口自身水箱独立工作,只有「归属哪个存储」需要核心),请把 Javadoc 改成这个事实并删掉死代码;否则请用 working 门控容器注册与能力暴露。

4. 流体伪槽位用「下标」当身份,sync 与点击之间列表一变就取错流体

客户端点击时把 FLUID_SLOT_BASE + index 发给服务端(StorageScreen 行 2254),服务端在 fillBucketFromStorage / takeFluidBucket重新 StorageFluidRegistry.collect(...) 再按同一 index 取(StorageServerStub 行 4721+)。collect() 的顺序取决于 livePorts 的 HashMap 迭代 + 「首次出现」顺序:只要中间发生了集合变化(端口被拆、区块卸载让 isLoaded 判负、新流体类型接入),后续 index 全部前移 → 玩家点的是「水」,服务端可能给「岩浆」,或直接静默失效(越界返回 EMPTY)。带 20 tick 校验间隔 + 区块卸载,这条路径是可达的。建议 RPC 里直接带流体身份(FluidStack 或 namespace+id+components),服务端按 isSameFluidSameComponents 匹配,而不是按下标。

5. 排序折算与 TODO 描述不一致(StorageServerStub 行 4644)

entry.amount() / FluidType.BUCKET_VOLUME,   // 1 B = 1 个物品

#4792 原文是「按数量排序时 1mB 相当于 1 个物品」。代码按 1 B = 1 个物品折算(128 B 端口 ≈ 128 个物品参与排序)。若 TODO 是笔误请顺手在 issue 里改掉;若确实要 1 mB = 1 物品,这里需要改。

6. 夹带与本 PR 无关的行为变更(同一提交 878c5f6c2

  • ItemSplitterBlock#getStateForPlacementgetHorizontalDirection().getOpposite()getHorizontalDirection()默认朝向整体翻转(正面由朝玩家改为背对玩家,潜行才反转),注释也被改写为「默认背面朝向玩家」。
  • ModBlocks.ITEM_SPLITTER.item().item(ChuteBlockItem::new),于是右键任何 IItemHandlerHolder/带物品能力的方块都会直接放置分配器(溜槽式放置)。

两者都改的是刚合并的 #4798 物品分配器,与流体端口无关。翻转朝向会改变老存档/老玩家的摆放习惯,属于行为不兼容,建议拆成独立 PR(或在描述里说明「修复 #4798 朝向反了」并给出理由)。

  • src/main/resources/assets/anvilcraft/textures/font/small.png(二进制)也被本提交改了。它是 SMALL_FONT所有仓储 UI 数字(含物品数量)都用它渲染,而 font/small.json 的字符表本来就含 A-Z/a-z/0-9,看不出是「补字形」。请在描述里说明这次改图的目的(或提供前后对比),否则这种全局性贴图变更应当单独成 PR。

💡 建议

  • StorageJeiSupport#hasFluidFor(行 456)名实相反:返回 true 的语义是「存储里没有该流体/该变体不是流体容器 → 需要现场盛装」,两个调用点都写成 if (hasFluidFor(...)) return false/continue;(行 217、438)。逻辑自洽但极易被下一个人改错,建议改名为 needsOnTheFlyFilling / lacksFluidFor
  • deposit(...) 的倾倒不受 all / matchesStorageItem 约束pourIntoFluidPort 在过滤之前执行,于是「只存入匹配物品」的左键也会把背包里所有流体容器倒进端口。如果这是有意的(倾倒属存储动作),建议在描述/提示里说明;否则应与过滤保持一致。
  • Ctrl+Z 撤销记录不含倾倒产物giveEmptiedContainerToStorageview.insert 把空桶入库,但没进 movedundo() 只会把 moved 里的物品取回,空桶留在存储里、流体也不回滚。若要求对称,需一并记录。
  • heightBias 不入 NBT:重载/重进后回到 0,且当流体量恰在 50~75% 区间内时 computeNextHeightBias() 会保持当前值 → 端口不再主动泵送(等于丢失了「正在保持水位」这个状态)。若这属于有意(偏置完全由水位推导),补一行注释即可。
  • markDirty 频率:偏置每次变化都 FluidNetworkManager.INSTANCE.markDirty,一次从空充满的爬升约 4~5 次全网络重建/端口;大管网多端口时值得实测一下(是否可合并到 VALIDATE_INTERVAL 节拍或做延迟合并)。
  • pourIntoFluidPort 的模拟只判成功不判用量tryEmptyContainer(..., Integer.MAX_VALUE, ...) 允许「部分倒入」,返回的容器仍带剩余流体却被当作「倒空后的容器」入库并播放倒桶音效——同流体端口快满时会产出「还剩 700 mB 的水桶」。建议按剩余容量判断,或明确接受并注释这一行为。
  • TerminalJeiTransferSupportif (deficit <= 0)if (deficit == 0) 是等价改写(deficit = Math.max(0, ...)),纯 diff 噪声,可去掉以缩小 diff。

🟢 看起来不错

  • 生命周期对称onLoadFluidNetworkManager.addContainer + setRemovedremoveContainer + StorageFluidRegistry.unregister,且沿用了仓库既有约定(其它流体 BE 同样用 addContaineraddContainerAfterLoadServerBlockEntityEventListener 统一处理),未新造模式。
  • 序列化完整saveAdditional/loadAdditional/getUpdateTag/getUpdatePacket 四条路径齐备且共用 TAG_TANK;空端口刻意不写 NBT,保持掉落物可堆叠(与 StoragePortBlockEntity.saveToDrop 的取舍一致)。
  • 客户端伪槽位隔离彻底applySyncResult / applyPreservedSyncResults(持序同步)都刷新 fluidsresetServerSlotshasContentsapplyPreservedSyncResultsgetStorageSlotapplySearchFilterrenderStorageContents 全部显式跳过 >= FLUID_SLOT_BASE,没有把伪槽位当物品读的路径。
  • interact 的越界安全:流体槽号(1<<24)在服务端不会落进 view.amount(slot) 这类真槽位分支(slot < view.size() 先判),伪造请求也只是 index 越界返回 EMPTY,不会抛异常;StorageInput.FLUID_BUCKET 追加在枚举末尾,FLUID_BUCKETvalidButtons 为 null(任意键合法),无序号兼容问题。
  • RPC 变更完整deposit/moveSameToStoragepour 参数后,客户端 stub 与全部调用点同步更新(已全仓库核对,无遗漏);StorageServerStub.interact 末尾重新读 containerMenu.getCarried(),修掉了「回传旧指针数量」的经典问题。
  • 流体系留物 / 指针守卫fillBucketFromStorage 在指针被占用时先于提示返回、drain 不足时把空桶还回(giveEmptiedContainer),consumeEmptyContainer 按「指针 → 背包 → 存储」顺序取桶,注释说明了理由。
  • isLoaded 守卫livePortsFluidNetworkScanner.addEndpointIfContainer 都先判 level.isLoaded(pos) 再取 BE,避免同步加载区块,符合本项目「区块卸载安全」要求。
  • 渲染抽取FluidTankRenderUtilinsetPixels 参数复用,insetPixels=0 时与旧实现完全等价(气体分支同样等效),端口窗口用 1.5 px 额外内缩;renderItemDecorations 的 push/pop 重排后仍然配平。
  • JEI 双向补齐:不仅放行了检查阶段(findMissingError/collectInputs),还在可用池里补了「空桶 + 流体」可盛装的成品桶并同步占位空桶,否则 getRecipeTransferOperations 仍会判缺料——注释把原因写清楚了,这点很少有人想到。
  • PlayerSetting.addCustom(ItemStack) 删除安全:全仓库仅剩 addCustom(ICategory) 的调用者(CategorySettingsScreenFilterCategory.from(filter) + addCustom(category)),无残留调用点;ModCategoryTypes.FILTERFilterCategory 均保留,FilterCategory 导入的移除是干净的。
  • 分类扩展到位ICategory.testFluid 默认 false 让流体天然不占物品类别,FluidCategory/NamespaceCategory/AndCategory/OrCategory 都实现了流体判定,matchesFluidCategoryFiltersALLOWLIST != testFluid 与物品侧 matchesFilters 写法一致。

结论: REQUEST_CHANGES — 核心功能(端口方块 + 管网高度偏置 + UI 流体伪槽位 + JEI 盛装)实现完整、与 #4792 的绝大部分要求一致,代码质量和注释密度都不错。但需要先处理:①端口改挂存储后旧登记不解除导致的跨存储流体读/写错位(含注册表 key 未含维度);②produceFilledContainer 失败回滚吞流体/吞空桶;③working 与类注释矛盾(或删死代码);④流体槽位按下标定位的身份不稳定性。另外请把物品分配器朝向/物品类变更与 font/small.png 这类无关改动拆出去。

建议标题(gh 未认证,未自动修改,请手动执行):

gh pr edit 4806 --title "Add Storage Fluid Port with storage fluid UI and JEI filling support 添加仓储流体端口及界面流体显示与JEI盛装交互"

🧪 测试建议(可选)

被测目标 推荐测试场景 优先级
StorageFluidRegistry.unregister/register 端口链 A→B 重挂后 collect(A) 不再含该端口;跨维度同坐标端口互不误删 🔴
StorageServerStub.produceFilledContainer 空容器数量/取用被并发或能力拒绝导致 removed < count 时,流体与空桶总量不变 🔴
fillBucketFromStorage 两次 sync 之间流体列表元素增删后点击,仍取到点击时的那种流体 🟡
StorageFluidPortBlockEntity.computeNextHeightBias fill=0 / 0.5 / 0.62 / 0.75 / 1.0 及 ±20 clamp 边界;区间内保持偏置不再变化 🟡
FluidAmountUtil.formatAmount / formatExactAmount 999 mB、1000 mB、1250 mB、≥1000 B 缩写、formatExactAmount(1575) 🟢
StorageFluidPortBlockEntity.loadAdditional 空/满/带组件的 tank NBT 往返;空端口掉落物与未放置物品可堆叠 🟡
pourIntoFluidPort 端口余量不足(部分倒入)、无可用端口(回退存桶物品)、批量倒桶只播一次音效 🟡

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched SKILL.md in skill 'pr-diff-scope-reconciliation' (1 replacement).

@PigeonNian
PigeonNian marked this pull request as draft September 11, 2026 11:35
@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806 converted_to_draft

操作: converted_to_draft 范围: 42 文件(31 Java / 12 新增 / 0 删除)/ 3748 行 diff
head: 377adff3(5 commits)— 与本轮之前的审查状态逐字节一致:42 文件 / 3748 行 / head SHA 全未变,本次事件只是「转为草稿」,没有新提交。此前提出的问题全部尚未处理,本轮按草稿阶段约定只列严重项。

标题未修改(gh auth status 显示 token 已失效,按纪律未导出 token 修复)。当前标题可用;若要更准确,建议:Add Storage Fluid Port with fluid pseudo-slots in storage UI 添加仓储流体端口(流体伪槽位 UI)

🔴 关键(转 ready 前必须处理)

  1. StorageFluidPortBlockEntity.validateLink() L210-229 — 改挂存储时不解除旧登记(跨存储读写他人流体)
    只有「解析不到核心 / 核心无 id」分支才 unregister(worldPosition)(L219);当端口重新挂到另一个核心时,直接 register(storage.getId(), …)(L226)并覆盖 this.storageId,旧 UUID 名下的条目永久残留unregister 只在 setRemoved() L202)。
    后果:collect(A) 把已属 B 的端口流体算进 A 的 UI;drain(A, …) / fillBucketFromStorage(A, …) 会真的从属于 B 的端口抽走流体。
    修法:register 之前先 unregister(this.worldPosition),或记录上一次的 storageId 按旧 id 定向移除。

  2. StorageFluidRegistry.PORTS key 形状与同级既有注册表不一致
    新表 = Map<UUID, Map<BlockPos, ResourceKey<Level>>>(UUID 优先、维度在最内层);同级 StorageBlockRegistry = Map<ResourceKey<Level>, Map<UUID, Set<BlockPos>>>维度优先,正是为避免坐标撞车,已在目标分支源码核对)。
    后果:同一 BlockPos 在不同维度、挂到同一存储 id 时互相覆盖(静默丢一个端口);unregister(BlockPos) 遍历所有存储删同一坐标(L920-927)→ 跨维度/跨存储误删。
    修法:照 StorageBlockRegistry 改成维度优先,unregister 增加维度参数。

  3. StorageServerStub.produceFilledContainer L2505-2524 — 部分失败路径吞资源(两条)

    • drained < perUnit * count → 直接 return 0drain() 已是 EXECUTE,已抽走的 drained mB 没有回灌,凭空消失(countProducibleContainers 之后端口被并发改动/卸载即命中)。
    • removed < count → 只把 removed 份流体灌回,已消耗的空容器不归还,且 acceptor.fill(...) 的返回值未检查。
      修法:改为逐份 drain;或失败时把已抽流体与已消耗空容器一并回滚(注释写「保持原子性」,实际不成立)。

⚠️ 建议

  1. 伪槽位用「下标」当身份:客户端把 FLUID_SLOT_BASE + index 发回,服务端 fillBucketFromStorage L4727-4733 重新 collect() 后按同一下标取。rememberedFluid 占位只解决了「取空导致前移」这一种,collect() 顺序仍取决于 livePorts 的 HashMap 迭代 + 首次出现顺序,区块卸载 / 端口增删依旧会让下标前移 → 点「水」拿到「岩浆」或静默失败。跨 RPC 身份建议用 FluidStack / ResourceLocation
  2. 排序折算与 issue 原文不符:实现 entry.amount() / FluidType.BUCKET_VOLUME(L4642,1 B = 1 物品),[TODO] 仓储流体端口 #4792 原文写「按数量排序时 1mB 相当于 1 个物品」——相差 1000 倍,请确认以哪边为准。
  3. working / getCoreMainPos() 是死状态:类 javadoc 声称「整个连通组件必须恰好接触一个核心才工作」,但 PR head 全树 grep 无任何 isWorking() / getCoreMainPos() 调用者(仅赋值 + @Getter)。请二选一:用它门控容器注册 / 能力暴露,或改注释 + 删死代码。
  4. 夹带改动未在描述说明(已在源分支上核实归属为本 PR 首个提交 878c5f6c2):ItemSplitterBlock.getStateForPlacement 默认朝向整体翻转(getHorizontalDirection().getOpposite()getHorizontalDirection(),老玩家摆放习惯变更)、ModBlocks.ITEM_SPLITTERChuteBlockItem::newtextures/font/small.pngSMALL_FONT,所有仓储 UI 数字都用它)。建议拆 PR 或在描述显式标注。
  5. 💡 6 个 src/generated/resources JSON(blockstates / item model / category/fluid / loot_table / recipe / minecraft 标签)缺文件末尾换行——建议确认是 runData 产物还是手写文件。

🟢 做得不错

  • 生命周期对称:onLoadFluidNetworkManager.addContainersetRemovedremoveContainer + unregister,沿用仓库既有约定。
  • 序列化四路径共用 TAG_TANK空端口刻意不写 NBT 以保持掉落物可堆叠,与物品端口 saveToDrop 取舍一致。
  • 越界安全:流体槽号 1<<24 不会落进 view.amount(slot) 等真槽位分支,伪造请求只是返回 EMPTY;StorageInput.FLUID_BUCKET 追加在枚举末尾(validButtons == null)无序号兼容问题。
  • 客户端伪槽位隔离彻底:applySyncResult applyPreservedSyncResultsresetServerSlotshasContentsgetStorageSlotapplySearchFilterrenderStorageContents 全部统一 >= FLUID_SLOT_BASE 分流。
  • JEI 两端都补齐(预检 countProducibleContainers + 可用池 addProducibleFluidContainers),不只放行检查阶段。
  • interact 末尾重读 containerMenu.getCarried(),修掉「分支改指针后回传旧数量」。

📋 声称验证表(issue #4792「[TODO] 仓储流体端口」)

声称 状态 对应实现
与仓储端口类似可互连延伸 StoragePortBlockEntity.findSoleCore / isPort 外提
128 B 单流体、拆除保留、门格海绵清除 CAPACITY_MBsaveToDropclearFluid
50%~75% 水位自动调等效高度(最多 20 米) computeNextHeightBias / heightBiasAt
UI 显示流体、不占类别、按数量排序 ⚠️ 折算 1 B = 1 物品,与原文 1 mB = 1 物品不符(见 5)
少于 1B 用 mB、达 1B 用 B、3 位有效数字 FluidAmountUtil
点击流体格铁桶取一桶 / 缺桶提示 fillBucketFromStorage + 两条提示文案
倒桶自动进同流体端口,无端口则存桶物品 pourIntoFluidPort + 左键/右键分流
JEI 识别空桶 + 流体现场合成盛装 ⚠️ 做齐但部分失败路径吞资源(见 3)

结论: COMMENT(草稿阶段) — 代码自上一轮起未变,上面 3 项 🔴 未修完前不建议转 ready;48 为建议项。修完 13 后可再触发审查复核。

复核说明:本轮同时把「新增 nullness 注解是否违规」按目标分支 AGENTS.md 重新核对——dev/1.21/1.6 规定必须用 javax.annotation.Nullable(strict non-null by default,全树 439 文件用 javax、0 文件用 JSpecify),故本 PR 新增的 3 处 javax 注解合规,不构成问题;唯一不一致是 StorageFluidPortBlock.java 用了 org.jetbrains.annotations.Nullable(💡 级一致性 nit)。


由 Hermes Agent 审查

- 引入 StorageFluidPortBlockItem,支持流体容量显示及物品内液面渲染
- 实现 StorageFluidPortItemRenderer,渲染物品内流体和模型玻璃窗口
- StorageFluidPortBlockEntity 新增存储流体端口连通性校验及注册机制
- 优化 StorageFluidRegistry,支持跨维度流体端口管理与流体匹配抽取
- StorageServerStub 支持按流体身份交互,增强流体桶操作的原子性与安全性
- StorageScreen 客户端交互改为发送流体身份,避免并发下流体对应错误
- ModelSelection 类使用模式匹配语法优化代码风格
- 添加渲染相关常量暴露,方便物品渲染统一内缩尺寸
- ServerLifecycleEventListener 中清理流体注册表,避免服务端残留数据
@PigeonNian
PigeonNian marked this pull request as ready for review September 11, 2026 13:58
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp && cat > /tmp/verify.sh <<'EOF'
EOF
echo "--- StorageJeiSupport ---"; sed -n '1,200p' /tmp/pr4806/35_a_src_main_java_dev_dubhe_anvilcraft_integration_jei_StorageJeiSupport.java

Reason: shell execution via heredoc

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806synchronize,仓储流体端口)

操作: synchronize(PR 仍为 draft
范围: 48 文件(36 Java / 14 新增 / 0 删除)/ 4242 行 patch-diff
本轮增量: head 377adff329d76957244,1 个提交、13 文件 +449/−93(feat(storage-fluid-port): 添加仓储流体端口与其客户端渲染和交互

✅ 上轮三项 🔴 复核 —— 均已实质修复

上轮问题 状态 复核依据
StorageFluidRegistry 改挂存储时旧登记不解除 ✅ 已修 表改为 Map<ResourceKey<Level>, Map<UUID, Set<BlockPos>>>(与 StorageBlockRegistry 同形,维度优先);unregister(@Nullable UUID, ResourceKey<Level>, BlockPos)validateLink() 注销旧 storageId、置 null,再重新 register;setRemoved() 带维度;ServerLifecycleEventListener 新增 clear()
produceFilledContainer 部分失败吞资源 ✅ 已修 两段 SIMULATE 预检(drain(..., true) + hasEnoughContainers)→ 先扣空容器(失败 giveBackContainers 还回)→ 再真 drain;并发不足时 giveBackContainers + refillFluid 灌回。差额与已耗材料都还清了
伪槽位用下标当身份 ✅ 已修 StorageClientStub.interact(..., FluidStack fluid)@CallableParam(OPTIONAL_STREAM_CODEC)),服务端统一 StorageFluidRegistry.find(id, fluid)isSameFluidSameComponents 匹配;客户端上报 getFluidSlot(slot).icon()。跨 RPC 身份已是实体而非下标

附带确认:上一轮 ⚠️「折算 1 B = 1 物品 vs issue 写 1 mB = 1 物品」已按 issue 改为 entry.amount()(mB);死状态 working / coreMainPos / getCoreMainPos() 已删除,findSoleCore + isPort() 提为 StoragePortBlockEntity 静态方法(两端口可互相延伸),StoragePortBlockEntity.validateLink() 仍在开头 working=false; coreMainPos=null,无回归。

⚠️ 建议处理(非阻塞)

  1. StorageJeiSupport#hasFluidFor 的 javadoc 与实现相反 — 实现是「仓储没有足量该流体 / 该物品不是流体容器 → 返回 true」,而本轮新增的 javadoc 写的是「仓储中存在足量(至少一桶)的同种流体」。两处调用点(if (hasFluidFor(...)) return false; / continue;)逻辑自洽,但注释与命名双反,后人按注释改动必然反转行为。建议改名(如 needsOnTheFlyFilling)并修正注释。
  2. 无关改动仍在 PR 内,用 blob 三点对比确认都来自本 PR 自己的提交(非继承自 dev):
    • commit 1:ItemSplitterBlock 默认朝向翻转(getOpposite() 去掉)、ModBlocks.ITEM_SPLITTER.item(ChuteBlockItem::new)font/small.png(401→958 B,所有仓储 UI 数字用它渲染);
    • 本轮:pipe_glass_node.png(306→301 B)、ModelSelectionBakeryinstanceof Fixed fixed 改为 record 解构 instanceof Fixed(SelectionPart part)(已核 AnvilLib 源码 record Fixed(SelectionPart part),语义等价、可编译,非编译错误)。
      PR 描述目前只有 resolved #4792,建议拆分提交或在描述里逐条说明这些改动的理由(字体若有新字形需求请写明)。
  3. 倾倒序列的 undo 不对称deposit / moveInventoryStackToStorage 的倾倒发生在 matchesStorageItem 过滤之前,且倒入产物不计入 moved,被倒空的容器由 giveEmptiedContainerToStorage 入库 → Ctrl+Z 无法恢复。注释已声明是有意设计,建议在描述/提示里明示。

💡 提示(细节)

  • StorageFluidPortBlockEntity 的 javadoc 称 FluidTank「锁定首个流体」;实际空罐可被另一种流体占用(findAcceptor 也正是靠这个才给空端口首次入液),措辞可收紧。
  • # 前缀搜索时流体条目会被服务端全部排除(addFluidEntries 只处理 @,客户端对 # 直接放行原序),即流体 tag 搜索缺失;建议对齐物品的 matchesTag 或注明不支持。
  • findAcceptor 在多个空端口间按 HashMap 迭代序任选(且可能跨维度选择),首次入液会占掉「恰好为空」的端口;建议稳定顺序或注释说明。
  • pourIntoFluidPort 返回值是「触及的桶数」:端口接近满时 FluidUtil 只转移部分 mB 也算 1 桶,且返回容器可能不是空桶(注释写作「空容器」)。目前只用于「是否改动」判定,建议注释收紧。
  • heightBias 不入 NBT(重载回落 0,若水位已在 5075% 区间则不再主动泵送),且每次偏置变化都 FluidNetworkManager.markDirty 触发全网重扫(一次充满约 45 次);建议节流或持久化。
  • FluidAmountUtil.formatAmount(0)"0 mB"(物品侧 0 直接显示 0);≥1000 B 先 (long) 截断再缩写(1999 B → 1K B)。显示细节,酌情统一。
  • nullness:本轮 3 处新增 javax.annotation.Nullable 与目标分支 dev/1.21/1.6AGENTS.md(strict non-null by default,要求用 javax)一致 ✅;唯一 nit 是 StorageFluidPortBlock.java 用了 org.jetbrains.annotations.Nullable,与同分支其它文件不一致,建议统一。

🟢 看起来不错

  • onContentsChanged 覆写已含 rememberFluid() + setChanged() + sendBlockUpdated(UPDATE_ALL) + StorageServerStub.onContentsChanged(storageId):流体变化既落盘又驱动仓储 UI 重同步。
  • FluidEntry / SyncResultStreamCodec.composite 参数序与 record 声明逐一对应(VAR_INT 数量 + OPTIONAL_STREAM_CODEC 图标),无字段序错位;10 个新增/修改 JSON 全部可解析,EOF 换行缺失仅出现在 datagen 产物(既有模式)。
  • 序列化四路径共用 TAG_TANK,物品渲染器/FluidTankItemTooltip 读取的 Tank/Fluid 键一致,掉落物保留流体且空端口不写 NBT 以保持可堆叠。
  • 客户端伪槽位隔离彻底:applySyncResultapplyPreservedSyncResults 都刷新 fluidsresetServerSlots / hasContents / getStorageSlot / applySearchFilter / 渲染 / getFluidSlotAt 统一按 >= FLUID_SLOT_BASE 分流;interact 末尾重读 containerMenu.getCarried()
  • 注册链完整:ModBlocks.item(StorageFluidPortBlockItem::new)ModBlockEntities.renderer(...) + .register()Capabilities.FluidHandler.BLOCK、创造标签、pickaxe tag、recipe/advancement/loot/blockstate/category 全部就位;StorageInput.FLUID_BUCKET 追加在枚举末尾,无序号兼容问题;TexturedButton 右键门控 isValidClickButtononClick 分支一致。

📋 声称验证表(issue #4792「[TODO] 仓储流体端口」)

声称 状态 对应实现
配方:空/潜影壳/空 + 储罐×3 + 空/潜影壳/空 → 1 个 recipe/storage_fluid_port.json(shulker_shell×2 + fluid_tank×3)
128 B 容积、单一流体、拆除保留流体 CAPACITY_MB = 128 * 1000TAG_TANK 四路径、getDrops/saveToDrop/playerWillDestroy
门格海绵右键清除流体 StorageFluidPortBlock.useItemOn + clearFluid()
可与仓储端口互相延伸连接 StoragePortBlockEntity.findSoleCore + isPort() 双方共认
等效高度调节维持 50%~75%、最多 20 米 computeNextHeightBias(LOW .5 / HIGH .75 / MID .625 / RATE 8 / MAX 20)
气体行为类似(模拟压力) FluidNetworkScanner.heightBiasAteffectiveHeight = Y + phi + bias(驱动气压推导)
UI 显示流体、不占类别、1 mB = 1 物品排序 addFluidEntriesentry.amount() 参与比较)、ModCategories.FLUID 仅默认列出
数量格式:<1B 用 mB、≥1B 用 B、小数三位有效数字 FluidAmountUtil.formatAmount / formatExactAmount
点击流体格用铁桶取一桶,缺桶弹提示 takeFluidBucket + fillBucketFromStorage + bucket_missing / not_enough
放入桶装流体时自动倾倒入端口,无端口则存桶物品 ✅(行为扩展) pourIntoFluidPort + findAcceptor空端口也接受,与「无相同流体就存物品」的字面略有出入,属合理扩展,建议注明)
JEI 合成识别「空桶 + 流体」并自动盛装 countProducibleContainers + produceFilledContainer + StorageJeiSupport.addProducibleFluidContainers(检查阶段与可用池两端都补齐)

🧪 测试建议

被测目标 建议场景 优先级
StorageFluidRegistry.unregister A 存储改挂 B 后 collect(A) 不含该端口;两维度同坐标互不影响;clear() 后无残留 🔴
produceFilledContainer SIMULATE 通过但 drain/容器不足时的回滚(容器与流体都归还);流体不足一桶的边界 🔴
流体伪槽位身份匹配 点击期间端口被拆 / 新区块卸载 / 接入新流体 → 按身份仍取到正确流体(不串流体、不发空请求) 🟡
FluidAmountUtil 0 / 999 / 1000 / 1575 / 999999 mB 的显示文本 🟡
JEI 空桶+流体 流体不足一桶、空桶被多槽位争用时不得重复计数 🟡

结论: COMMENT(草案阶段) — 上一轮三项 🔴 均已修复且有据可查,本轮未发现新的阻塞问题;剩余为命名/注释、无关改动说明与显示细节类建议。建议在转 ready_for_review 前处理第 1、2 项(尤其 hasFluidFor 的反向注释与夹带改动的描述)。

PR 标题「Added Storage Fluid Port. 添加了仓储流体端口」准确且已中英双语,未修改标题(仅建议去掉句末句点以符合 <英文标题> <中文标题> 惯例)。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

审查完成。以下是完整审查意见(已核对 PR head 全量源码 + 目标分支 dev/1.21/1.6 基线,以及 issue #4792 原始需求)。


代码审查摘要 — PR #4806

操作: ready_for_review
范围: 48 个文件(36 Java / 14 新增 / 0 删除)/ 4242 行 diff(+2814 / −114)
目标: fluidtank/1.21/1.6dev/1.21/1.6,声称 resolved #4792


🔴 关键(必须修复后再合并)

StorageServerStub.java — 终端取出路径会对流体伪槽位做物品索引,抛 IndexOutOfBoundsException

createOrder()(L4640)现在会通过新增的 addFluidEntries()(L4690)把 StorageFluidRegistry.FLUID_SLOT_BASE + index1 << 24 起)追加进排序结果。有两处服务端消费者直接拿这个 order 去索引 StorageView,没有过滤伪槽位:

方法 位置 循环
terminalExtractFirst(创造终端取出) L3089,循环 L3116 int index = order.getInt(i); long stackAmount = view.amount(index);
extractFromTerminal(由 TerminalItem.removeOne 调用,普通终端取出) L4004,循环 L4014 同上

StorageView.amount/resource/extract(L5216 / L5220 / L5269)实现都是 this.entries.get(index) —— 索引 1 << 24 必然越界:

for (int i = 0; i < order.size(); i++) {
    int index = order.getInt(i);
    long stackAmount = view.amount(index);   // ← index = 16777216 → IndexOutOfBoundsException

触发条件(不需要特殊配置): 终端目标存储挂着有液体的流体端口,且排序把流体条目排在首个可取出物品条目之前

  • SortMode.COUNT + OrderMode.REVERSE:流体按 1 mB = 1 物品折算,满端口 = 128000,数量倒序时必然排最前 → 每次取出都炸;
  • SortMode.NAME:如 Lava 排在 Stone 之前即可命中;
  • 默认分类都是 CategoryMode.UNLIMITEDCategoryEntry.mode 默认值),matchesFluidCategoryFilters 直接放行,搜索框为空也放行,所以默认设置下流体条目确实会进 order。

服务端在交互/RPC 里抛出未捕获异常(终端取物失败、日志刷屏,视上层包装还可能断连)。

修复建议: 两处循环加守卫 if (index >= StorageFluidRegistry.FLUID_SLOT_BASE) continue;。但更值得做的是消除这个类别问题:把伪槽位混进 order 后,每个 order 消费者都必须记得过滤——本 PR 已在 StorageScreen 补了 6 处守卫(render/mouseClicked/applySearchFilter/resetServerSlots/hasContents/applyPreservedSyncResults),仍然漏了这 2 处。建议把 order 拆成「物品 order + 流体条目」两条数据,或让 createOrder 提供一个仅供物品路径使用的入口。


⚠️ 警告

  1. 终端 UI 未接入流体(同一根因的另一面)。 terminalReorder(L2901)返回的 order 含流体伪槽位,但客户端 TerminalRemoteOverlayCONTENTS.get(slot) 取内容、取不到就跳过(applyClientFilter 亦同),结果流体在终端浮窗里既不显示也不可点。请明确终端是否需要显示流体;若需要,必须与上面的守卫一并处理。

  2. 夹带了与流体端口无关的既有行为改动,建议拆分 PR / 至少在描述中列出:

    • ItemSplitterBlock.getStateForPlacement 默认朝向反转getHorizontalDirection().getOpposite()getHorizontalDirection()),且 ModBlocks.ITEM_SPLITTER 的物品类从默认 BlockItem 换成 ChuteBlockItem(右键带物品容器的方块时强制放置)。这改变的是既有方块的摆放/交互手感,老玩家的摆放习惯会变,与本特性无关。
    • ModelSelectionBakery 4 处 instanceof ModelSelection.Fixed fixed → record 解构模式,纯重构。
    • craftBudget()(约一组按产物堆叠上限折算)、consumeCraftingInput 剩余物安置重写、Shift+双击批量移入、refillAndCollectSlots 语义调整——这些是仓储/合成系统的既有修复,并非 [TODO] 仓储流体端口 #4792 内容,建议在描述中单独列出(有利于 changelog 与回归定位)。
    • PlayerSetting 删除 addCustom(ItemStack):已确认仓库内无其它调用者(仅 addCustom(ICategory) 在用),删除安全。
  3. 二进制资源改动缺少说明: textures/block/pipe_glass_node.png(新方块模型并未引用它,管道外观受影响)与 textures/font/small.png(尺寸仍 39×42、font/small.jsonchars 未变,即字形映射没变,但像素被重画)。两者都会全局影响管道贴图与所有小字号数量文本,请确认是有意调整。

  4. StorageFluidPortBlock.useItemOn:手持门格海绵但内部为空时 clearFluid() 返回 false,仍然返回 sidedSuccess(吞掉这次点击);建议无变化时返回 PASS_TO_DEFAULT_BLOCK_INTERACTION 之外的明确语义,避免与后续交互冲突。


🟢 看起来不错

  • 注册完整性齐全ModBlocks(含 .blockstate/.tag/.recipe/.item)、ModBlockEntitiesvalidBlocks + renderer)、CapabilitiesEventListenerCapabilities.FluidHandler.BLOCK)、创造栏(FunctionalBlocks + FunctionalBlocksSections)、ModCategoryTypes/ModCategories + 数据包 category/fluid.json、配方(loader 与生成 JSON 一致)、loot_table、blockstate、minecraft:tags/block/mineable/pickaxe、lang(en_usen_ud 逐条反向一致,占位符 %s 保留)。
  • 生命周期对称onLoadFluidNetworkManager.addContainersetRemovedremoveContainer + StorageFluidRegistry.unregisterclear() 挂在 onServerStopped,静态表不会跨世界残留。
  • 序列化完整saveAdditional/loadAdditional/getUpdateTag/getUpdatePacket 齐全,流体变化 onContentsChangedsendBlockUpdated + 通知仓储 UI;saveToDrop 对空罐不写 BLOCK_ENTITY_DATA(保证与未放置过的物品堆叠)是正确细节。
  • 等效高度偏置按 [TODO] 仓储流体端口 #4792 实现且收敛正确:50%~75% 区间内保持偏置(避免在区间边界反复横跳)、区间外按 |误差| × 8 步长调整并 clamp ±20、空罐归零、clamp 后无变化不再 markDirty。已确认 effectiveHeight 全仓库只在 FluidNetworkScanner.addEndpointIfContainer(L342)计算一次,偏置同时覆盖液体与气体(effectiveHeight - Y 推导气压),没有漏掉的第二条路径。
  • JEI/自动补料先模拟后执行produceFilledContainer(先 drain(SIMULATE) + hasEnoughContainers,再「先扣容器后抽流体」并在失败时成对回滚)、countProducibleContainerstransferMaterialExact 的回滚记账(moved - fromStorage - fromFluid)都自洽,未发现失败路径下的物品/流体凭空增减;hasFluidFor 语义虽反直觉(返回 true 表示缺少流体),但两处调用一致,行为正确。

📋 声称验证表(#4792 逐项)

需求 状态 对应实现
空/潜影壳/储罐×3 配方得 1 ShapedRecipeLoader.storageFluidPortrecipe/storage_fluid_port.json 一致
可连集装箱/存储站,双向延伸连接关系 StoragePortBlockEntity.findSoleCore + isPort(含 StorageFluidPortBlock)共用扫描
128 B 单流体、拆除保留、海绵清除 CAPACITY_MB = 128 * 1000saveToDrop/getDrops/playerWillDestroyclearFluid
50%~75% 等效高度自适应,±20 m,越远越快 computeNextHeightBias + ADJUST_RATE/MAX_HEIGHT_BIAS + FluidNetworkScanner 偏置
气体同样通过模拟压力维持 同一偏置驱动 FluidPipeNetwork 的气压项(L559–560、L643)
UI 显示流体、不占类别、1 mB = 1 物品排序 StorageFluidRegistry.collect + FluidCategory + addFluidEntriesentry.amount() 直接参与比较)
数量 <1 B 用 mB、≥1 B 用 B、三位有效数字 FluidAmountUtil.formatAmount / formatExactAmount
点击流体格用桶装出,缺桶给提示 takeFluidBucket + bucket_missing/not_enough 提示
放桶装流体自动倾倒,无对应端口则存桶物品 pourIntoFluidPort + 左/右键区分(左键倒、右键存物品)
JEI 填充合成识别 空桶 + 流体 StorageJeiSupport.producibleFromFluid / addProducibleFluidContainerswithdrawNeedsFromStorages 现场盛装

另:PR 标题基本准确,仅建议统一为 Add Storage Fluid Port 添加仓储流体端口(现为 "Added Storage Fluid Port. 添加了仓储流体端口")。本次 gh auth status 显示 /opt/data/home/.config/gh/hosts.yml 中 token 已失效,故未自动修改标题。


🧪 测试建议

被测目标 建议场景 优先级
extractFromTerminal / terminalExtractFirst 目标存储挂有含液体的流体端口 × 各 SortMode × OrderMode(含 REVERSE),确认 order 中的伪槽位被跳过、终端能正常取出物品 🔴
StorageFluidPortBlockEntity.computeNextHeightBias 空罐 / 恰好 50% / 75% / 边界值 ±20 clamp / 多次 tick 后是否收敛不震荡 🟡
StorageFluidRegistry.drain(…, simulate) + produceFilledContainer 跨端口凑量、模拟通过但 EXECUTE 失败、容器扣取失败的回滚 🟡
FluidAmountUtil 999 mB / 1000 mB / 1024 mB / 128000 mB / 1234567 mB 的单位与有效数字 🟢

结论: REQUEST_CHANGES — 功能实现完整、对 #4792 的需求逐项落实且细节(偏置收敛、失败回滚、序列化/同步)质量较高;但必须修复终端取出路径对流体伪槽位的越界索引(🔴),并建议把 ItemSplitter 朝向/物品类改动、ModelSelectionBakery 重构、合成系统相关修复从本 PR 中拆出,二进制贴图改动请补充说明。

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched references/shell-only-diff-navigation.md in skill 'github-webhook-pr-review' (1 replacement).

- 在StorageScreen中新增浮层提示组件,实现点击处提示流体交互失败原因
- 设计浮层显示优先级及位置,避免被界面其他元素遮挡
- 修改StorageServerStub,流体交互函数返回包含失败原因的结果
- 新增FluidNotice枚举,统一管理流体交互失败提示文本
- 细化装桶逻辑,提前判定空桶存在性并反馈具体提示
- 确保交互失败原因可传至客户端,由界面自行渲染提示信息
- 避免仓储界面中动作栏消息被界面覆盖,改用浮层提示机制
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp/pr4806; cat > ext.sh <<'EOF'
#!/bin/bash
FILE="$1"
START=$(grep -nF " b/$FILE" pr4806.diff | grep ":diff --git" | head -1 | cut -d: -f1)
if [ -z "$START" ]; then echo "NOT FOUND: $FILE"; exit ...

Reason: shell execution via heredoc

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@PigeonNian
PigeonNian marked this pull request as draft September 11, 2026 14:43
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -sL "https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792" | head -c 1500; echo; echo "=== PR ==="; curl -sL "https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806" | python3 -c "...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -sL "https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['body'])" 2>/dev/null | head -60

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806(草稿)

操作: converted_to_draft(草稿态,按约束只报实质问题,省略风格建议)
范围: 48 文件(36 Java / 14 新增 / 0 删除),+2983 / −126
基线核对: merge-base = 4c8481a6#4798 溢流溜槽与物品分配器),分支 7 个提交
关联: resolved #4792([TODO] 仓储流体端口,XeKr)— 逐项核对见下方验证表


🔴 关键(建议合并前修复)

1. 普通文本搜索时服务端丢弃流体条目,导致「按名称搜索流体」永远搜不到,客户端对应代码成为死代码

StorageServerStub.addFluidEntries()(PR head 约 3809 行):

boolean matches = search.isEmpty()
    || search.charAt(0) == '@'
       && id.getNamespace().toLowerCase(Locale.ROOT).contains(search.substring(1));

对照物品侧 matchesFilters()(同文件 5120 行)的实现:

boolean matchesSearch = search.isEmpty()
    || search.charAt(0) == '@' && id.getNamespace()...contains(...)
    || search.charAt(0) == '#' && item.tags().anyMatch(...)
    || search.charAt(0) != '@' && search.charAt(0) != '#';   // ← 普通文本:服务端放行,交给客户端

流体这条少了「普通文本 → 返回 true」与 # tag 两个分支。后果链:

  • 服务端 createOrder() 在普通搜索(如输入「水」)时不会把流体槽位放进 order
  • 客户端 StorageScreen.applySearchFilter() 里专门为流体写的那段(entry.icon().getHoverName() / BuiltInRegistries.FLUID.getKey(...).getPath() 匹配)只过滤已经在 order 里的槽位,而 rebuildDisplayOrder(false) 正是 applySearchFilter(order);折叠路径的 appendFluidSlots()require this.order.contains(slot)
  • 于是该分支永不可达,搜「水/water」时流体在 UI 里直接消失,与 addFluidEntries 自己的 javadoc「普通文本搜索由客户端按本地化名称过滤」自相矛盾。

建议直接把 matchesFilters 的搜索判定抽成共用方法(含 # 流体 tag 匹配),避免两侧再次漂移。


⚠️ 警告

2. StorageJeiSupport.hasFluidFor() 语义反转、命名误导(当前行为侥幸正确)

// 名为 "has fluid for",实际:有流体时 return false,缺流体/非流体容器时 return true
if (entry.amount() >= content.getAmount() && isSameFluidSameComponents(...)) return false;
return true;

两个调用点都靠「取反」写法抵消:if (hasFluidFor(...)) return false; / if (hasFluidFor(...)) continue;。当前结果正确(我逐分支推演过:非容器 → 早退 false;储量不足 → 早退 false;储量足够 → 继续走空容器与配额判断),但任何下一个调用者都会按名字写出反向逻辑。建议改名为 lacksFluidFor/needsFluid,或直接反转函数体、把调用点的 if (…) continue 改成 if (!…) continue

3. 夹带了与「仓储流体端口」无关的改动,建议拆分

夹带改动 归属于 影响
ItemSplitterBlock.getStateForPlacement 朝向翻转(getHorizontalDirection().getOpposite()getHorizontalDirection())+ ModBlocks.ITEM_SPLITTER 的物品类型 .item().item(ChuteBlockItem::new) 878c5f6c 已有方块行为变更ChuteBlockItem.onItemUseFirst 会在被点击方块具备 ItemHandler 时改为 useOn(直接放置),与分配器朝向改动叠加,需要单独的测试与说明
ModelSelectionBakeryinstanceof ModelSelection.Fixed(SelectionPart part) record 模式重构 d7695724 与流体端口无关,纯语言层改写
PlayerSetting.addCustom(ItemStack) 删除 + FilterCategory 导入清理 377adff3 死代码清理,混在 fix(renderer) 提交里
craftBudget()64/perCraft → 按 maxStackSize)+ consumeCraftingInput/refillAndCollectSlots/autoRefillCrafting 剩余物重写 878c5f6c 与「桶在合成里」相关,可留在本 PR,但改动量大,建议在描述里单独说明并给出验证方式(不可堆叠产物现在只合成 1 个,是行为变更)

4. heightBias / rememberedFluid 都不持久化

rememberedFluid 不写 NBT 已在 javadoc 声明是有意的;heightBias 没有说明——重载/区块重载后偏置归零,管道网络会按真实 Y 重扫,10 tick 内重新收敛。若确认可接受,建议在字段 javadoc 里补一句,免得后续被当 bug 报。

5. StorageFluidRegistry.positions(UUID) 标注「供测试与调试使用」,但本 PR 未附任何测试

PR 覆盖了序列化往返(Tank)、网络 codec、合成剩余物、等效高度闭环调节等易错路径,建议至少补上:register/unregister 对称与跨维度隔离、collect() 同类流体合并与取空占位、computeNextHeightBias 的死区/±20 clamp、FluidAmountUtil 各档位,否则该 helper 属于无消费者的公开 API。

6. 流体伪槽位编号依赖 collect() 的列表顺序

槽位 = FLUID_SLOT_BASE + index,而 index 来自 livePorts() 遍历 HashMap<维度, Map<UUID, Set<BlockPos>>>(HashMap/HashSet 迭代序),且 ordercreateOrder 时)与 fluidssync 时)是两次独立 collect()。点击路径已用流体身份 isSameFluidSameComponents 兜住服务端错配,但渲染侧仍可能瞬时把 A 流体的图标画到 B 的槽位。建议给流体槽位一个稳定映射(如按流体 id 排序或取最小坐标端口为代表)。

7. TerminalJeiTransferSupport.producibleFromFluid() 只看空容器、不看流体储量

javadoc 已说明「是否真有流体由服务端决定」,但 JEI 侧因此可能出现「可转移」假阳性、实际转移失败。若非有意保留,建议同时查 screen.getFluids()StorageJeiSupport.producibleFromFluid 就是这么做的)。


🟢 看起来不错

  • [TODO] 仓储流体端口 #4792 要求逐条对应:配方(空/潜影壳/空 + 三储罐)、与仓储端口双向延伸连接、128 B 单流体、拆除保留流体、门格海绵清除、50%~75% 目标区间 + 最远 20 格等效高度调节、1 mB = 1 物品排序、mB / B / 三位有效数字显示、点击装桶 + 缺桶提示、倒桶自动倾入同流体端口(无端口回退存桶)、JEI 空桶+流体现场盛装。
  • StoragePortBlockEntity.findSoleCore() 抽取等价(已与目标分支逐行对账:cores.size() != 1 → nullvisitedPorts < CONNECTIVITY_LIMITisPort() 覆盖两类端口均一致)。
  • 注册/生命周期对称:onLoad → FluidNetworkManager.addContainersetRemoved → removeContainer + StorageFluidRegistry.unregisterServerStoppedEvent 清静态表,与项目内 FluidTankBlockEntity 等既有模式一致。
  • StorageFluidPortBlockgetDrops / getCloneItemStack / playerWillDestroy(创造模式非空掉落)完全沿用 StoragePortBlock 的既有写法;空罐不写 BE 数据以保持可堆叠,考虑周到。
  • 交互失败提示走界面浮层而非动作栏(仓储界面开着时动作栏被遮挡)——设计正确;FLYOUT_Z=300 的层次与两次 flush() 的排序理由在注释里交代清楚。
  • StorageFluidPortBlockItem / 渲染器 / FluidTankItemTooltipTank→Fluid 结构一致;小字体 small.json 已声明 0-9 . B m K M,新数量文本可渲染(纹理同步扩容)。
  • 生成文件「无结尾换行」与目标分支既有 datagen 输出一致(已实测 storage_port.json 同样以 } 结尾),非问题

📋 声称验证表

声称 状态 对应实现
resolved #4792:方块 + 配方 + 连接延伸 StorageFluidPortBlock/BlockEntityShapedRecipeLoader.storageFluidPortfindSoleCore/isPort
128 B 单流体、拆除保留、门格海绵清除 CAPACITY_MBsaveToDrop/getDropsclearFluid
等效高度 50%~75% / 最多 20 格 / 变化率随偏差 computeNextHeightBias(死区保持、Math.clamp(±20)
UI 显示流体、1 mB = 1 物品排序 addFluidEntriesentry.amount() 直接入 OrderEntry
数量显示 mB/B/三位有效数字 FluidAmountUtil.formatAmount/formatExactAmount
点击装桶 + 缺桶提示 takeFluidBucket/fillBucketFromStorage + FluidNotice.BUCKET_MISSING
倒桶自动倾入 + 无端口回退存桶 pourIntoFluidPort/findAcceptor,失败回退 view.insert
JEI 空桶 + 流体识别并填充 StorageJeiSupport/TerminalJeiTransferSupport/produceFilledContainer
UI 中按名称搜索流体 见 🔴 1(服务端普通搜索丢弃流体条目,客户端过滤分支不可达)

结论: COMMENT(草稿态) — 功能与 #4792 要求基本对齐、生命周期与注册完整,但 🔴 1 使「流体搜索」这条设计好的路径实际失效(客户端已有代码却永远走不到),建议修复后再转 ready;同时建议拆分第 3 条列出的无关改动(尤其是 ItemSplitter 朝向/物品类型的既有方块行为变更)以便分别验证。

建议标题(gh auth status 显示 token 已失效,本次未自动修改):
Add Storage Fluid Port and fluid storage interactions 添加仓储流体端口与仓储流体交互

🧪 测试建议

被测目标 推荐场景 优先级
StorageServerStub.addFluidEntries 普通文本 / @ns / #tag 搜索下流体条目必须保留(回归 🔴 1) 🔴
StorageFluidRegistry register↔unregister 对称、跨维度同坐标隔离、clear()、同类流体合并与取空占位 🔴
StorageFluidPortBlockEntity saveAdditional/loadAdditional 往返、getUpdateTagTank、空罐不写 NBT、setRemoved 后注册表无残留 🔴
computeNextHeightBias 0.5/0.75 死区、±20 clamp、空罐归 0、偏差越大步长越大 🟡
FluidAmountUtil 999 mB / 1000 mB / 1.25 B / 1 000 000 mB(K 缩写) 🟡
pourIntoFluidPort / fillBucketFromStorage 无端口→存桶、仅剩 250 mB→提示缺桶优先、指针被占→不消耗空桶 🟡
StoragePortBlockEntity.findSoleCore 0 / 1 / 2 个核心、端口链互相延伸、超 CONNECTIVITY_LIMIT 🟢

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp/pr4806 && (python3 -c "import PIL; print('PIL ok')" 2>&1 | tail -1); (cd /opt/data/workspace/AnvilCraft && git show 4c8481a60:src/main/resources/assets/anvilcraft/textures/font/small.png) > ba...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/storage-rpc-gui-review.md in skill 'minecraft-mod-pr-review' (1 replacement). · Patched references/pr4806-storage-fluid-port-review.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched SKILL.md in skill 'pr-diff-scope-reconciliation' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

审查完成。以下是审查意见(直接输出,未执行任何 gh 评论命令;gh auth status 显示 token 已失效,故未自动改标题)。


代码审查摘要 — PR #4806

Operation: synchronize(head 60b3e2fa / base d442720b,7 commits,48 文件,+2983/−126,mergeable: true)
范围: 36 Java + 9 生成资源 + 1 数据模型 + 2 纹理
验证方式: patch-diff 下载 → 目标分支 dev/1.21/1.6 交叉核对 API(IFluidHandlerHolder / FluidNetworkManager / FluidHandlerWrapper / StorageAccessValidator)、逐 commit 归属确认、issue #4792 规格逐条比对

🔴 关键(需修复后合并)

1. StorageServerStub.addFluidEntries — 输入任意普通搜索词后,所有流体条目从仓储界面消失
StorageServerStub.java:4769

// 普通文本搜索由客户端按本地化名称过滤;服务端只处理 @ 前缀
boolean matches = search.isEmpty()
    || search.charAt(0) == '@'
       && id.getNamespace().toLowerCase(Locale.ROOT).contains(search.substring(1));
if (!matches) { continue; }

物品路径 matchesFilters() 的等价判断多一个兜底分支

|| search.charAt(0) != '@' && search.charAt(0) != '#';   // 普通文本全部放行,交给客户端二次过滤

缺了这一条 → 搜索框里输入 water / 等普通文本时,matches 恒为 false,流体条目在服务端就被剔除(不进 order),客户端的 applySearchFilter 那道按本地化名称过滤的流体分支(同 PR 新增)永远不会被触发。现象:一打字流体格全部消失,清空搜索才回来。注释声称「普通文本由客户端过滤」,实现与注释自相矛盾。

boolean matches = search.isEmpty()
    || search.charAt(0) == '@'
       && id.getNamespace().toLowerCase(Locale.ROOT).contains(search.substring(1))
    || search.charAt(0) != '@' && search.charAt(0) != '#';   // ← 补齐

# 前缀搜索对流体无意义,按物品路径放行后由客户端按名称过滤即可,或显式在 # 时排除流体并注明。)


⚠️ 警告

2. StorageJeiSupport.hasFluidFor() 名称与文档同实现语义相反(当前逻辑正确,但极易被「修」成 bug)StorageJeiSupport.java:456
javadoc 写「仓储中存在足量同种流体时返回 true」,实现却是「找不到该流体(或不是流体容器)时返回 true / 找到时返回 false」。两个调用点都是按「true = 仓储里没有这种流体」在使用(:217 提前 return false:438 continue),因此目前行为正确——但方法名 + 文档都在反向暗示,下一个人「修正」调用点就会把整个功能做反。建议改名(如 lacksFluidInStorage / isFluidMissingFromStorage)并同步 javadoc。

3. JEI 补库路径的新增分支未做背包空间兜底,可能吞掉「空桶 + 流体」StorageServerStub.java:3215

player.getInventory().add(resource.copyWithCount(produced));

produceFilledContainer 已经消耗了空桶并抽走了端口流体,随后 Inventory.add 放不下时不会掉落、只会剩下 copy 内未放入的部分被丢弃。紧邻的下一个循环恰恰显式做了这件事(getInventorySpace + 「为防背包放不下导致 add 丢弃」注释),新增分支与之不一致。建议复用 giveEmptiedContaineraddItem(...) || Block.popResource(...) 兜底,或先 getInventorySpace 校验。

4. 端口挂载关系变化不通知仓储 UI,条目最多滞后到下一次流体数量变化
StorageFluidPortBlockEntity.validateLink()(:210)在改挂/断开时 StorageFluidRegistry.unregister/register,但没有 StorageServerStub.onContentsChanged(id)。服务端 stub.orders 只由 onContentsChanged 清空 + 版本自增(StorageServerStub.java:3688),而流体伪槽位是否显示取决于该缓存顺序。场景:界面开着时把端口的连接链路断开/重新接上、或端口从 A 存储改挂 B 存储,而水箱内容恰好没变 → 流体条目不会立即出现/消失(可能留下一个空网格格)。建议在 StorageFluidRegistry.register/unregister 成功后调用 StorageServerStub.onContentsChanged(storageId)

5. 夹带了与标题无关的行为变更,建议在描述中说明或拆分
逐 commit 核对后这些改动确为本 PR 7 个 commit 引入(非 merge 带到 diff 里):

  • ItemSplitterBlock.getStateForPlacementgetHorizontalDirection().getOpposite()getHorizontalDirection()(朝向整体反转),且 ModBlocks.ITEM_SPLITTER.item()ChuteBlockItem::new(获得「对着容器直接放置」的 onItemUseFirst)。这改变的是已有方块的放置朝向与物品行为,与本 PR 主题无关,是最需要确认的一条「顺手改动」。
  • ModelSelectionBakeryinstanceof Fixed fixed → 记录模式 Fixed(SelectionPart part)(纯重构,4 处)。
  • StorageScreen Shift+双击批量移入(lastQuickMoved/findInventorySlotWith)、craftBudget 按产物堆叠上限折算。
    Added Storage Fluid Port 这个标题不足以覆盖上述内容;改标题/补描述时请把这几项列出来(StorageScreen 的右键入库、TexturedButton 右键支持与本特性强相关,可不列)。

💡 建议

  1. FluidNetworkManager.INSTANCE.markDirty(level)StorageFluidPortBlockEntity:161):偏置每变化一次就使整张流体网缓存失效,端口在 50%–75% 区间外的调整期内会持续触发每 tick 一次全网重扫。大基地(数百管道)下建议按 tick 节流或仅在偏置跨过某个粒度(如 1 格)时置脏。
  2. computeNextHeightBias()tank.isEmpty() 时直接 return 0(:166)。按 issue 描述「少于 50% 就开始降低等效高度」,空端口保持在 0(比 10% 水位的 −20 更「高」)看起来是刻意收敛行为,但会让空端口吸引流体的力度弱于仅装 1 mB 的端口——是否有意?若是,建议在 javadoc 里写明。
  3. TerminalJeiTransferSupport.producibleFromFluid 只校验「有空容器」,不校验「存储里有该流体」,与 StorageJeiSupport.producibleFromFluid(有 hasFluidFor 校验)不一致 → 终端路径的 JEI 预检可能过于乐观(灰不掉的「可转移」按钮,转移阶段静默缺料)。两处建议统一。
  4. StorageFluidRegistry.positions()(:266)javadoc 写「供测试与调试使用」,但仓库无 src/test 目录、diff 中无任何调用方 → 死代码。要么删除,要么随测试一起加(drain 的两阶段模拟/回滚、collect 的同流体合并是最该测的两处)。
  5. craftBudget javadoc 说「镐子等不可堆叠物品…一次点击只合成 1 个」,但 Math.max(1, perStack/perCraft) * Math.max(1, multiplier) 在空格 8 倍下是 8 个。文档与实际不符,建议补一句「乘以放大倍率」。
  6. 两张纹理:textures/font/small.png 由 palette(colortype 3)重编码为 RGBA(colortype 6,401→958 B,尺寸 39×42 未变)、textures/block/pipe_glass_node.png 内容变更(306→301 B)。小字号字模影响所有仓储数量渲染,若只是编辑器重新保存建议 revert 以保持 diff 干净;若是有意修字形,请在描述中说明(对应 font/small.json 的字模网格未变,行/列不会错位)。
  7. PR 描述写 resolved #4792——GitHub 只识别 fixes/closes/resolves 才自动关单,目前 issue [TODO] 仓储流体端口 #4792 仍为 open。想要自动关单请改成 fixes #4792

🟢 看起来不错

  • 生命周期对称onLoadFluidNetworkManager.addContainer / setRemovedremoveContainer + StorageFluidRegistry.unregister,与 FluidTankBlockEntity 既有模式一致;ServerLifecycleEventListenerclear(),静态表有回收路径。
  • 序列化完整saveAdditional/loadAdditionalTank 对称读写;getUpdateTag 附带 tank(客户端 UI 与渲染一致);ClientboundBlockEntityDataPacket 具备;空端口不写 BE 数据以免破坏堆叠(saveToDrop 的细节考虑到位)。
  • FluidEntry(icon, amount) 分离设计:数量 0 的 FluidStack 会丢流体类型、无法过网,用 icon(保留非空流体身份)+ 独立 amount=0 解决占位显示,并且 FluidStack.OPTIONAL_STREAM_CODEC 不会把 icon 编码成空——这个坑避得很干净。
  • FluidTankRenderUtil 重构等价性:新增 insetPixels 后,insetPixels=0inset == TANK_WminY == insetheight == 1-2*inset,气体分支与液体分支的盒体坐标与重构前逐一对应,既有储罐渲染无回归。
  • 流体伪槽位隔离完备serverSlots/resetServerSlots/hasContents/applyPreservedSyncResults/getStorageSlot/applySearchFilter 全部显式跳过 >= FLUID_SLOT_BASEFLUID_SLOT_BASE = 1<<24 不会与真实槽位冲突;点击时按流体身份(而非下标)上报,StorageAccessValidator 不校验槽位范围,链路自洽。
  • StorageFluidRegistry.drain 两阶段(SIMULATE→EXECUTE)+ 回滚(先扣容器、失败退回容器 + 灌回流体,produceFilledContainer),并且「不先模拟就 EXECUTE 会导致凭空增减物品」的推理是正确的。
  • consumeCraftingInput 剩余物修复:原实现「2 个水桶 + 剩余物」会把整槽替换成 1 个空桶导致物品丢失;新实现按原版 ResultSlot.onTake 规则分三种情形(剩余物落槽 / 同为催化剂净变化 0 / 交还玩家或存储),并保留 changed=false 防无限产出。
  • matchesFluidCategoryFiltersALLOWLIST != testFluid(...) 与物品侧语义一致FluidCategory.test 恒 false + testFluid 判定、NamespaceCategory 覆写 testFluidAnd/Or 递归覆写——分类体系扩展完整,en_us/en_ud 键对称(含 upside-down 词序反转)。
  • 配方与 issue 规格一致空/潜影壳/空 + 储罐×3/空/潜影壳/空 得 1,unlockedBy 与 advancement、loot table、pickaxe tag、blockstate/model、创造栏(FunctionalBlocks + FunctionalBlocksSections)、Capabilities.FluidHandler.BLOCK 注册、JEI 分类语言键全部齐备,无 ghost 文件、无 TODO/调试残留、无硬编码凭据。

📋 声称验证表(PR 声称 resolved #4792 vs. issue 规格)

issue #4792 要求 状态 对应实现
方块 storage_fluid_port + 配方(空/潜影壳/空、储罐×3、空/潜影壳/空 得 1) ModBlocks.STORAGE_FLUID_PORTShapedRecipeLoader.storageFluidPort、generated recipe/advancement/loot/pickaxe
可连集装箱/存储站,且与仓储端口互相延伸连接 StoragePortBlockEntity.findSoleCore + isPort 双向识别
128 B 容积、每端口单一流体、拆除保留流体 CAPACITY_MB = 128*1000FluidTank(首流体锁定)、saveToDrop + playerWillDestroy(创造)
门格海绵右键清除流体 StorageFluidPortBlock.useItemOn + clearFluid()
非气体/气体均靠等效高度维持在 50%–75%,最多 ±20 ADJUST_FILL_LOW/HIGH=0.5/0.75MAX_HEIGHT_BIAS=20FluidNetworkScanner.heightBiasAt(气体走 effectiveHeight - Y 气压)
UI 显示流体、不占类别、按数量 1 mB = 1 物品排序 FLUID_SLOT_BASE 伪槽位 + addFluidEntriesentry.amount() 直接参与比较)
<1 B 用 mB、≥1 B 用 B、小数保留三位有效数字 FluidAmountUtil.formatAmount/formatExactAmount(≥1000 B 额外并入 K/M 缩写,属扩展)
点击流体格自动用铁桶装一桶,缺桶弹提示 fillBucketFromStorage + FluidNotice.BUCKET_MISSING/NOT_ENOUGH + 界面浮层
放入桶装流体时自动倾倒入同流体端口,无端口则存桶物品 ✅(有扩展) pourIntoFluidPort + findAcceptor(同流体端口优先,其次空端口,再退回普通入库)
JEI 填充合成识别「空桶 + 流体」并自动盛装 StorageJeiSupportTerminalJeiTransferSupportproduceFilledContainer
普通文本搜索下流体仍可用名称过滤 见 🔴 1

🧪 测试建议

被测目标 推荐测试场景 优先级
StorageServerStub.addFluidEntries 空/@ns/普通文本/#tag 四种搜索下流体条目是否入 order(回归 🔴 1) 🔴
StorageFluidRegistry.drain(id, fluid, mb, simulate) 单端口不足需跨端口凑量;SIMULATE 不改状态;EXECUTE 后余额守恒 🔴
produceFilledContainer 空容器取自背包/存储/指针三来源;容器不足、流体不足、drain 中途不足三条回滚路径数量守恒 🔴
StorageFluidRegistry.collect 多端口同流体合并;取空端口以 0 占位且下标不前移;端口改挂后不残留旧存储 🟡
StorageFluidPortBlockEntity.computeNextHeightBias fill=0/0.49/0.5/0.625/0.75/0.76/1.0 与 ±20 截断;区间内保持不变 🟡
StorageServerStub.SyncResult / FluidEntry StreamCodec 往返(含 amount=0 占位条目不丢流体类型) 🟡
StorageServerStub.interact 流体格左键/右键/Shift、指针被占用、无端口可倾倒时各分支的 changednotice 🟡
consumeCraftingInput 剩余物 2 水桶 + 空桶剩余物、催化剂(净变化 0 返回 false)、单水桶 + 空桶 🟢
StorageFluidRegistry.register/unregister 维度隔离(同坐标不同维度互不影响)、clear() 后无残留 🟢

结论: REQUEST_CHANGES — 功能实现与 issue #4792 的规格高度吻合,生命周期/序列化/回滚/伪槽位隔离都做得扎实,值得肯定。但 🔴 1(普通搜索会隐藏全部流体条目)是用户可直接触发的功能缺陷,建议修复后再合并;⚠️ 2–5 中 2、3 建议一并处理,4、5 至少需要在 PR 描述中交代(尤其 ItemSplitterBlock 朝向反转属已有方块的行为变更)。

标题建议gh 未认证,本次未自动修改):
Add Storage Fluid Port 新增仓储流体端口
若按上条把夹带改动也写进标题,可考虑:Add Storage Fluid Port and Fluid Storage UI 新增仓储流体端口与流体仓储界面


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'texture-asset-pr-review' (1 replacement). · Patched references/pr4806-storage-fluid-port-review.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched references/shell-only-diff-navigation.md in skill 'github-webhook-pr-review' (1 replacement).

Pigeon_Nian added 3 commits September 11, 2026 23:09
- 修改合成格清空顺序,先送入存储站,存储站满后回退玩家背包
- 新增returnToStorageOrInventory方法,确保物品优先送入存储站
- 调整合成物品转移相关注释,提高描述准确性
- 细化排序方法,新增createItemOrder用于仅包含真实物品槽位排序
- 细化createOrder方法参数,支持包含或排除流体伪槽位
- 流体伪槽位排序仅用于界面渲染,避免索引越界问题
- 修正部分注释,提升代码可读性和维护性
- 统一物品归还流程,新增 returnItems 方法处理多级归还路径
- 替换旧方法调用,增加玩家参数以便准确处理物品归还
- 优化物品放回存储和背包的顺序与兜底逻辑,避免物品丢失
- 调整容器归还逻辑,新增对玩家实例的支持与统一处理
- 生产桶装流体时改用玩家实例,确保物品正确发放与回收
- 删除冗余旧方法,简化代码结构和调用关系
- 修改遍历循环仅遍历主背包槽位,避免将物品放入盔甲槽或副手槽
- 统一使用 Inventory.INVENTORY_SIZE 替代 getContainerSize() 减少错误
- 确保物品叠加与取用逻辑仅作用于主背包,避免材料统计和扣除不一致
- 保持与原版Inventory#add逻辑一致,防止堆叠与空槽判断异常
- 修复因盔甲槽错误遍历导致的物品放置与材料扣除问题
@PigeonNian
PigeonNian marked this pull request as ready for review September 11, 2026 15:28
@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'anvilcraft-power-storage-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /opt/data/workspace/AnvilCraft; B=refs/remotes/pr4806/target; echo "=== SMALL_FONT 定义 ==="; git show $B:src/main/java/dev/dubhe/anvilcraft/client/gui/screen/StorageScreen.java | grep -n "SMALL_FONT...

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806

操作: synchronize | 范围: 48 文件(36 Java、9 生成资源、3 源资源;14 新增)/ +3066 −155 | CI: build ✅ checkstyle ✅ | 关联 #4792

本 PR 实现了 #4792「仓储流体端口」的全部要点。核心实现质量不错:注册表按维度分层、点击按流体身份而非下标匹配、装桶走「模拟→扣容器→执行→失败回滚」、双写序列化齐全、伪槽位在客户端各物品路径都做了跳过。以下是需要修的点与建议。


⚠️ 1. 倾倒流体被记成「物品入库」,导致 Shift+Z 撤销错误 — StorageServerStub.quickMoveToStorage

int inserted = moveInventoryStackToStorage(player, view, slot, true);   // 允许倾倒
if (inserted > 0) { moved.merge(key, inserted, Integer::sum); changed = true; }
...
StorageServerStub.recordUndo(stub, moved);

moveInventoryStackToStorage(..., pour=true) 在倾倒成功时返回的是桶数return poured;),于是 moved 里被记成「1 个水桶已入仓储」。实际发生的是「流体倒进端口 + 空桶回仓储」,仓储里并没有这个桶物品。而 undo()extractByResource(view, resource, …) 从仓储按物品取回 —— 玩家会把仓储里原本存在的同种桶(自己没存过的)当成撤销拿走,等于一笔幽灵移动。

同 PR 的 moveSameToStoragedeposit 都明确处理了这一点(注释:桶装流体优先自动倾倒:属于存储动作而非入库,故不记入 moved),只有 quickMoveToStorage 漏了。

建议:让 moveInventoryStackToStorage 只返回「入库物品数」(倾倒返回 0),倾倒结果单独回传/只置 changed;或在该路径内先判 FluidUtil.getFluidContained(stack) 分流。

⚠️ 2. 缺 onChunkUnloaded 注销 → 残留条目可让「A 存储界面读走 B 存储的流体」— StorageFluidPortBlockEntity

该 BE 只在 onLoad() 注册、setRemoved() 注销,未覆写 onChunkUnloaded();而项目既有约定是区块卸载也注销(StorageBlockEntity#onChunkUnloadedStorageBlockRegistry/TerminalBlockRegistry.unregisterIfApplicable)。区块卸载后重载是新 BE 实例storageId 为 null,validateLink()unregister(this.storageId, …)(null → no-op)清不掉旧条目。若端口区块卸载期间链路被改接到另一存储(链路可跨区块,改链只需相邻区块已加载),重载后端口在 B 下重新登记,而 (A, pos) 旧条目永久残留:A 的 UI 会显示该端口流体,fillBucketFromStorage(A) / pourIntoFluidPort(A) 还会真的读写它 —— 跨存储误归属(读与写都错)。

建议:① 覆写 onChunkUnloaded() 做与 setRemoved() 相同的注销;或 ② 注销改为「按位置清该维度所有存储下的条目」,去掉「必须知道旧 storageId」这一约束。

⚠️ 3. StorageJeiSupport.hasFluidFor 名实相反

/** 该物品是否为装有流体的容器,且仓储中存在足量(至少一桶)的同种流体。 */
private static boolean hasFluidFor(...) {
    if (content.isEmpty()) return true;                     // 非流体容器 → true
    for (...) if (amount >= need && same) return false;     // 仓储有该流体 → false
    return true;                                            // 仓储没有 → true
}

文档说「有该流体时为 true」,实现恰好相反。两个调用点(producibleFromFluidaddProducibleFluidContainers)都按「true = 需要现场盛装」使用,当前行为是对的,但名字/注释与语义相反(该 commit message 还写着"修正流体检测判定错误"),后续极易被"顺手改回"从而反转整条生产逻辑。建议改名(如 needsFluidProduction)并同步 Javadoc。

⚠️ 4. 范围外变更(建议拆 PR 或至少在描述里说明)

  • ItemSplitterBlock.getStateForPlacementgetHorizontalDirection().getOpposite()getHorizontalDirection(),即翻转物品分配器默认朝向(潜行反向),与刚合并的 Added Overflow Chute and Item Splitter添加了溢流溜槽和物品分配器 #4798(其注释"潜行时反转向,便于朝着自己或背着容器摆放")正好相反;同 commit 还把 ModBlocks.ITEM_SPLITTER.item() 换成 .item(ChuteBlockItem::new)(改放置交互)。这会让新放置与已放置的分配器朝向不一致。
  • pipe_glass_node.png 被重绘(23/256 像素,颜色由 (208,234,233) 改蓝灰 (123,174,183))。该贴图只被 models/block/glass_pipe_node.json 使用 —— 即改动影响既有玻璃管道节点外观,而本 PR 端口模型只用 storage_port / storage_fluid_port 两张贴图,疑似误带入。
  • font/small.png 被重编码(索引色 401 B → RGBA 958 B)并多出 1 个不透明像素(约 x=5, y=10,位于第二行字形 'B' 附近),与主题无关,建议确认是否有意。
  • 另有一大批非本主题修正(合成余量/归还 returnItemscraftBudget、背包遍历 getContainerSize()INVENTORY_SIZE 的盔甲槽修复等)。这些修正本身看起来是正确的,但标题/描述完全没有体现——PR 描述目前只有一行 - resolved #4792

💡 建议(非阻塞)

  • 伪槽位=fluids 列表下标FLUID_SLOT_BASE + index 依赖 collect() 的 HashMap/HashSet 遍历序,且服务端 createOrdersync 是两次独立 collect()。点击按身份匹配、图标/数量也取自客户端那份,故不会取错流体;但排序位置可能漂移(缓存 order 与最新 fluids 不一致时该槽不显示)。想要更稳可用稳定 key(fluid id + components)替代下标。
  • refillFluid 是尽力而为:真实抽取失败回滚时若同存储端口一时装不下,抽出的流体会被丢弃(非原子回滚)。极端并发会丢流体,建议记日志或还成实物桶。
  • TerminalJeiTransferSupport.producibleFromFluidStorageJeiSupport 那份重复实现,且只校验空容器、不校验流体(JEI 预检通过而服务端产不出时会静默无反应);两边可共用 helper,并把「流体够不够」纳入预检以便给提示。
  • deficit <= 0 → == 0deficit = Math.max(0, required - have) 已非负,两者等价(commit 声称修逻辑错误,实际不可达),保留即可,不必当作修复。
  • PlayerSetting.addCustom(ItemStack) 是公开 API 删除(仓内无调用方,删得合理),建议在描述里提一句以免影响附属模组。
  • 新增默认分类 FLUID 只对新建 PlayerSetting 生效(无迁移);因默认 CategoryMode.UNLIMITED 不过滤,流体仍会显示,故仅是「老玩家分类面板少一个 Fluids 快捷项」的小缺口。
  • 手持门格海绵清除流体时未播 SoundEvents.SPONGE_ABSORBLargeCauldronBlock 同类分支有播),建议对齐。
  • heightBias 不持久化(重载后从 0 收敛),且 ADJUST_RATE=8 / ADJUST_INTERVAL=10 最坏每 0.5 s 变 5 格等效高度,建议附一次液/气实测(与泵、多层容器共存是否抖动)。

🟢 看起来不错

  • 注册与数据齐全且符合 datagen 约定:ModBlocks(noOcclusion + requiresCorrectToolForDrops + pickaxe tag)、ModBlockEntities(validBlocks + renderer)、Capabilities.FluidHandler.BLOCK、创造栏与分段、category/fluid.json、配方与 [TODO] 仓储流体端口 #4792 的「空/潜影壳/空 + 储罐×3」一致、掉落/进度/语言(en_ud 逐字反转且占位符保留);新增生成 JSON 末尾无换行与既有生成文件一致(datagen 正常行为,非问题)。
  • 交互安全性:点击上报流体身份,服务端 find()isSameFluidSameComponents本存储端口集合内匹配,越权动别的存储不可能;提示优先级(缺桶 > 不够一桶)合理,并用 FluidNotice 浮层而非动作栏(会被界面盖住)。
  • 客户端伪槽位防护很全resetServerSlots / hasContents / applyPreservedSyncResults / rebuildFoldedGroups / getStorageSlot(返回 null)/ applySearchFilter(按流体名与 id path)逐处跳过,未发现 NPE 或越界;createItemOrder 让取物路径从源头不含伪槽位,比"每个消费者记得过滤"稳。
  • 服务端取物/合成produceFilledContainer 的顺序(模拟→扣容器→抽流体→校验→回滚)正确,transferMaterialExact/transferMaterial 的返回值被写回槽位,现场盛装的桶不会凭空消失;returnItems 三级兜底(主→次→掉落)避免吞物品;interact 结尾重读 carried 修掉了旧数量回传。
  • FluidTankRenderUtil.drawFluidInTank(..., insetPixels) 默认 0 与旧行为逐坐标等价(含气体分支),端口渲染复用同一内缩常量,物品/方块液面一致。
  • 浮层 z 序(300)+ graphics.flush() 时机、renderSlotCount 抽取、TexturedButton 右键支持等自洽,未见回归。

📋 声称验证表(对照 #4792

要求 状态 证据
storage_fluid_port 方块 + 指定配方 ModBlocks / ShapedRecipeLoader / recipe json
可连集装箱/存储站,且与仓储端口可互相延伸 findSoleCore 静态化 + isPort()
128 B、单一流体、拆除保留、海绵清除 CAPACITY_MB=128000、FluidTank、saveToDrop/getDrops、MENGER_SPONGE 右键
50~75% 水位调节、最多 20 米、越远越快 ADJUST_FILL_LOW/HIGHMAX_HEIGHT_BIAS=20、step ∝ |error|
气体同样按模拟压力维持 FluidNetworkScanner 的 effectiveHeight 推导同时作用于气体
UI 显示流体、不占类别、1 mB=1 物品排序、单位规则 StorageFluidRegistry.collect / FLUID_SLOT_BASEaddFluidEntriesFluidAmountUtil
点击流体格用铁桶装一桶、缺桶提示 FLUID_BUCKET / takeFluidBucket / FluidNotice.BUCKET_MISSING
桶装流体自动倾倒,无对应端口则存桶物品 pourIntoFluidPort + 回退 view.insert
JEI 填充合成识别空桶+流体 StorageJeiSupport / TerminalJeiTransferSupport + 服务端 produceFilledContainer
「resolved #4792 ⚠️ issue #4792 仍为 open;resolved 不是 GitHub 关闭关键字,建议写 Resolves #4792

结论:REQUEST_CHANGES

功能与 #4792 完全对齐、CI 通过、实现细节质量高。请先处理 ⚠️1(倾倒计入 undo)、⚠️2(区块卸载未注销注册表)、⚠️3(hasFluidFor 名实相反),并把 ⚠️4 的范围外变更(物品分配器朝向/物品类、共享贴图)在描述里说明或拆出。

标题建议:当前标题准确描述主功能但未覆盖随带修正,建议 Add Storage Fluid Port 添加仓储流体端口(或更完整:Add Storage Fluid Port and storage/crafting fixes 添加仓储流体端口及仓储/合成修正)。本次 gh auth status 显示 token 失效(Active account: Gugle2308, invalid token),按流程执行 gh pr edit,故以文字给出建议。

🧪 测试建议

被测目标 建议场景 优先级
quickMoveToStorage / undo Shift+左键倾倒流体桶 → Shift+Z 撤销(仓储内另有同种桶)应无错位撤销 🔴
StorageFluidPortBlockEntity 注册表 端口区块卸载期间改链路 → 重载后仅在新存储可见;旧存储 UI/drain 不应触及 🔴
produceFilledContainer 空桶不足 / 流体不足一桶 / 中途失败:容器与流体都应回到原处,不增不减 🔴
takeFluidBucket / fillBucketFromStorage 指针空/持桶/持他物、跨多端口凑满 1 B、并发取空 🟡
FluidAmountUtil 0 / 999 / 1000 / 1234 / 1500 / 128000 mB 与 formatExactAmount 的进位与有效数字 🟡
序列化 带流体物品的拆放(含创造模式与 Ctrl 中键克隆)、区块重载、Tank 缺失容错 🟡
heightBias 液/气两态在含泵与多层容器网络中从空/满收敛到 50–75% 且不振荡 🟡
StorageScreen 伪槽位 折叠/搜索/Shift 取液/流体取空后列表不跳位、不越界、不 NPE 🟡

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806

操作: converted_to_draft(草稿状态,按约定仅报重要问题,风格建议从略;结论为 COMMENT,不阻塞)
范围: 48 个文件(36 Java,14 新增,0 删除)/ 4722 行 diff
目标分支: fluidtank/1.21/1.6dev/1.21/1.6(1.21.x 项目,已跳过 26.1 API 迁移检查)
关联 issue: #4792 [TODO] 仓储流体端口(当前 state = open,标签 📋️ TODO)
核对依据:git fetch origin dev/1.21/1.6 后在 FETCH_HEAD 上交叉验证(findSoleCoreInventory.INVENTORY_SIZEStoragePortBlockFluidNetworkManager 等),非本地陈旧克隆。


⚠️ 需要处理的问题

1. 改动范围远超标题与描述(最需要处理的一项)
标题/描述只说「添加仓储流体端口」,但 diff 里混入了大量与本功能无关的改动,多为对现有玩法行为的变更,建议拆分或在描述中列全:

无关改动 位置 性质
放置朝向反转 getOpposite()getHorizontalDirection() ItemSplitterBlock.getStateForPlacement 🔴 现有方块行为变更,且与仓库内 17+ 个方块的主流约定(getHorizontalDirection().getOpposite())相反
ITEM_SPLITTER 物品改为 ChuteBlockItem ModBlocks 现有物品行为变更(右键容器可直接放置)
连合成预算 64 / perCraft → 按产物堆叠上限折算 craftBudget(),3 处调用 现有手感变更:不可堆叠产物(镐子等)从最多 64 次变为 1 次
合成剩余物处理重写 + returnCraftingRemainder consumeCraftingInput 影响桶/碗等剩余物去向
clearCrafting 归还顺序:背包优先 → 存储优先 clearCrafting 用户可见变更(JEI 转移后材料改落到仓储)
剩余物占格时先收走再补料 autoRefillCraftingrefillAndCollectSlots 连续合成语义变更
getContainerSize()INVENTORY_SIZE 共 5 处 多个方法 独立 bugfix(物品/材料误入盔甲槽),值得单独 PR
仓储界面 Shift+双击批量移入(lastQuickMoved/findInventorySlotWith StorageScreen 新功能
浮层 flyout 重构 + z 序/flush 处理 StorageScreen 渲染重构
record 模式重构、addCustom(ItemStack) 删除、<= 0== 0 ModelSelectionBakeryPlayerSettingTerminalJeiTransferSupport 纯清理(已验证 addCustom(ItemStack) 在目标分支无调用方,安全)
二进制纹理改动 textures/block/pipe_glass_node.pngtextures/font/small.png 与流体端口无关:前者是玻璃管道外观,后者 font/small.jsonchars 未变(. m B K 等字形本已存在)故非新增字形所需。请确认是有意修改

2. StorageJeiSupport.hasFluidFor 名字与语义相反(易埋雷)

// 名字读作「有该流体」,实际 return true 表示「仓储里没有可用的该流体」
if (StorageJeiSupport.hasFluidFor(screen.getFluids(), variant)) return false;  // producibleFromFluid
if (StorageJeiSupport.hasFluidFor(fluids, variant)) continue;                  // addProducibleFluidContainers

content.isEmpty() 分支也返回 true。两处调用都靠「取反使用」才正确,后续任何一次顺手使用都会写反。建议改名(如 lacksFluidFor/needsProducingFromFluid)或反转返回值。

3. findAcceptor#4792 规格不一致
规格:"自动将流体倾倒入有相同流体的仓储流体端口中,没有对应的端口则存入对应的桶物品"
实现:无同种流体端口时回退到任意空端口(并把该端口永久锁定为该流体),桶不再作为物品入库。请确认这是有意的放宽(空端口可接收任意流体)还是应严格按规格退回物品存储。

4. 等效高度调节缺少「已接入管网」守卫(规格限定 + 性能)
规格限定:"储存了非气体流体且通过管道连接了其他流体储存方块时" 才调节。实现里只要 tank 非空就调节(不看是否接管道、不区分气体)。后果:无管道连接的满水端口在水位偏离 50%~75% 时,每 10 tick 改变一次 heightBiasFluidNetworkManager.markDirty(level)tickLevel该维度全量 rebuild()(逐网络 FluidNetworkScanner.scan + 玻璃管显示迁移)。偏置 clamp 到 ±20 后自终止(约 7 次),但属于白白重扫;建议加「是否已接入管网/其他容器」守卫。


💡 建议(非阻塞)

  • FluidCategory 与「不占类别」[TODO] 仓储流体端口 #4792 写「在 ui 中显示对应流体,不占类别」,实现新增 category.anvilcraft.fluid 并加入 PlayerSetting 默认 listed(默认 UNLIMITED 不过滤,无副作用)。请确认这是有意的额外过滤能力。
  • heightBias 不持久化:重载后从 0 重新调节(10 tick 内到位),期间有一次等效高度跳变。若在意"存档前后一致"可考虑写入 NBT。
  • livePorts 每次调用遍历所有维度,而 collect() 在每次 sync、每次 find/drain/findAcceptor 都会跑一遍;端口数量大时可考虑缓存/按维度直查。
  • 仓库无 src/test 源集,故建议改为手测清单(草稿阶段):①无核心时端口独立被桶/管道读写;②端口从 A 存储改挂 B 存储后 A 界面不再显示、drain(A) 不抽到该端口;③取空后条目占位显示 0 且槽位编号不整体前移;④区块卸载/超维存储站跨维度;⑤删掉端口方块后 StorageFluidRegistry 无残留(positions());⑥装载入后直接取空的占位流体名。

🟢 看起来不错

  • findSoleCore 抽取 + isPort 双类型判断,仓储端口 ↔ 流体端口双向延伸完整;StoragePortBlockEntity.validateLink 重构后与旧实现等价(cores.size()!=1 → null,不写 coreMainPos/working)。
  • 交互按流体身份匹配isSameFluidSameComponents)而非下标,明确规避了「点击与处理之间列表变化导致取到别的流体」。
  • createItemOrder(排除伪槽位)专供取物路径,从源头避免 StorageView 越界;FLUID_SLOT_BASE + index 在服务端 collect / 客户端 fluids / 折叠路径三处索引基准一致;伪槽位在 contentsserverSlotshasContentsapplySearchFilterapplyPreservedSyncResults 等所有遍历点都有 skip 守卫。
  • 装桶失败采用「先模拟再执行 + 失败 giveEmptiedContainer 回滚」;produceFilledContainer 严格「先扣容器后抽流体」,失败时 giveBackContainers + refillFluid 双向回滚 —— 不会吞物品/流体;transferMaterial(Exact) 把盛装量计入 moved 后由调用方写入槽位,账目守恒。
  • 空端口不写 BlockEntityTag(保证与未放置过物品可堆叠)、创造模式 playerWillDestroy 掉落带流体端口(模仿原版潜影盒),与同类 StoragePortBlock 逐行一致。
  • 数据生成完整:blockstate/物品模型/配方/成就/战利品表/mineable/pickaxe/anvilcraft/category/fluid.json + en_us/en_ud 均已同步(FluidNotice 的两个 key 也存在)。
  • Capabilities 注册(IFluidHandlerHolder)、BE 渲染器注册、onLoadaddContainer / setRemovedremoveContainer+unregister 生命周期对称 ✅

📋 声称验证表

声称 状态 说明
添加仓储流体端口 StorageFluidPortBlock/BlockEntity/BlockItem、128 B 单流体、拆除保留、门格海绵清除、配方(潜影壳+3 储罐+潜影壳)、UI 流体格(1 mB=1 物品、mB/B/三位有效数字)、JEI 空桶+流体盛装 —— 均与 #4792 相符
resolved #4792 ⚠️ 主体实现,但有两处与规格不符:findAcceptor 空端口兜底(应按规格退回桶物品)、等效高度调节未限定「已通过管道连接其他流体储存方块」
交互:点击流体格装桶 + 缺桶提示 FLUID_BUCKET/QUICK_MOVE_FROM_STORAGE 分支 + FluidNotice.BUCKET_MISSING/NOT_ENOUGH 浮层提示

结论: COMMENT(草稿状态不阻塞)— 功能主体实现质量较高、与 #4792 基本对齐;合并前请先处理「范围外改动/纹理改动」的拆分或说明,以及问题 2–4(命名反向、findAcceptor 规格偏差、高度调节守卫)。

✏️ PR 标题

gh auth status 显示 The token in /opt/data/home/.config/gh/hosts.yml is invalid(未认证),按 webhook 约定未执行 export GITHUB_TOKEN/source .env,故本次未自动修改标题,仅给出建议:

  • 拆分无关改动:Add Storage Fluid Port 添加仓储流体端口
  • 保持当前范围:Add Storage Fluid Port and storage fluid handling 添加仓储流体端口与仓储流体处理

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'pr-fix-claim-verification' (1 replacement). · Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806

操作: synchronize(已按流程完成全量审查)
范围: 48 个文件(36 Java / 14 新增文件 / 0 删除)/ 约 +3071 −155 行
分支: fluidtank/1.21/1.6dev/1.21/1.6(1.21.x 常规 feature PR,已跳过 26.1 API 迁移检查)

已核对 diff(patch-diff)、PR head ce22079b 的完整源文件、以及目标分支 dev/1.21/1.632da04af)的既有实现交叉验证;gh 当前未认证(token invalid),故未自动改标题,建议标题见文末。


🔴 关键

  • StorageServerStub.terminalReorder()rpc/StorageServerStub.java:2942)把流体伪槽位发给了远程终端,但终端渲染层完全不认识它们 — 该方法调用的是含流体伪槽位的 4 参 createOrder(...)(第 4721 行 → includeFluids = true),而 TerminalRemoteOverlay(本 PR 未改动)只按真实物品槽位工作:CONTENTS.getOrDefault(slot, EMPTY)FLUID_SLOT_BASE + n 恒为空 → 终端界面末尾会多出 N 个空白格,翻页/滚动/光标范围也把它们算进去。副作用是普通文本搜索路径下 syncFullContents 每次都会把这些永远拿不到内容的槽位重新计入 missing,白白消耗页预算。
    安全侧没问题:syncterminalTaketerminalTakeToInventory 都有 index/slot >= view.size() 守卫,不会越界崩溃(这点做得对)。
    建议:terminalReorder 改用 createItemOrder(...)(与 terminalExtractFirst/extractFromTerminal 一致);若确实想让终端显示流体,需要同时补上终端的流体渲染与内容同步。

⚠️ 警告

  • ItemSplitterBlock.getStateForPlacementblock/ItemSplitterBlock.java:57)朝向语义整体反转,与本 PR 主题无关getHorizontalDirection().getOpposite()getHorizontalDirection(),默认朝向与潜行朝向同时互换(原注释"潜行时反转向,朝着自己或背着容器摆放"也已失效)。这会影响所有玩家的分拣器摆放习惯与既有机器布局;同时 ModBlocks.ITEM_SPLITTER.item() 改为 .item(ChuteBlockItem::new)ModBlocks.java:1869),使分拣器可以像溜槽一样贴容器摆放。两者都是有意的改动(commit message 有写),但与"新增仓储流体端口"无关,PR 描述里也没有提。建议拆成独立 PR,或至少在描述中列为独立变更。
  • src/main/resources/assets/anvilcraft/textures/block/pipe_glass_node.png 被改动,但新方块并未使用该纹理 — 二进制变更,逐像素比对:23/256 像素不同((208,234,233)(168,208,217)(168,208,217)(123,174,183) 整体变暗,另有 2 个像素由透明变为不透明)。全仓引用只有 models/block/glass_pipe_node.json,而 storage_fluid_port.json 的纹理是 storage_port + storage_fluid_port。疑似误提交,建议还原或说明是否为玻璃管道的有意调色。
  • StorageFluidPortBlockEntity.clearFluid()block/entity/StorageFluidPortBlockEntity.java:236)未清 rememberedFluid — 门格海绵清空后,StorageFluidRegistry.collect() 仍以 0 数量保留旧流体条目(占位逻辑见 rememberFluid/getRememberedFluid),点击该格只会得到 NOT_ENOUGH 提示,且会一直残留到重载或装入另一种流体。TODO 对该动作的语义是"清除流体",此处与"取空后保留 0 条目"(面向抽取路径)混用了同一套记忆。建议 clearFluid() 同时清空记忆。
  • StoragePortBlockEntity.findSoleCore 扫描对未加载区块调用 level.getBlockState(neighbor),无 isLoaded 守卫 — 该缺陷在仓储端口中原本就存在(目标分支 StoragePortBlockEntity.java:349),但本 PR 将其静态化后新增了一个调用者:流体端口每 20 tick(VALIDATE_INTERVAL)就会走一遍端口链扫描。服务端 getBlockState同步加载未加载区块,属已知的 C2ME 兼容/卸载死锁风险,本 PR 扩大了触发面。建议在 findSoleCore 的邻居循环里补 level.isLoaded(neighbor) 守卫。

💡 建议

  • StorageJeiSupport.hasFluidFor()integration/jei/StorageJeiSupport.java:456)名实不符:实际返回 true 表示"不是流体容器或仓储中不足量",与名称及 Javadoc 恰好相反,两个调用点(producibleFromFluidaddProducibleFluidContainers)都靠取反使用。逻辑目前是对的,但这颗雷留给下一位维护者踩的概率很高,建议改名为 cannotProduceFromFluid(...) 或反转返回语义。
  • quickMoveToStorageStorageServerStub.java:433-435)把倾倒的桶数记入了 undo 的 movedmoveInventoryStackToStorage(..., true) 返回的是 poured 桶数,随后 moved.merge(key, inserted, ...),而 moveSameToStorage/deposit 的注释明确约定"属于存储动作,不记入 moved"。当前 undo() 只从仓储实际取出(countInStorage 返回 0 会跳过),不会凭空多出桶,但语义已经分裂,建议统一为不记入。
  • pourIntoFluidPort 在循环外只取一次 findAcceptor:第一个可接收端口装满即 break,即使仓储里还有另一个能存同种流体的端口也不再尝试;批量倒桶时会少倒。建议每轮重新取 acceptor(成本也只是遍历已加载端口)。
  • NamespaceCategory.testFluid 未判空,FluidCategory.testFluid!fluid.isEmpty() 守卫 — 两个实现不对称;当前调用方只传非空 icon 故安全,建议统一加守卫以免日后被别处复用。
  • addFluidEntries# 前缀搜索会整体剔除流体(注释说明流体无标签语义,matches 最后一条分支只放行非 @/#)——行为自洽,但玩家看到的是"搜 # 时流体全部消失",可在工具提示或文档里提一句。
  • adjustHeightBias 每次偏置变化都 FluidNetworkManager.markDirty(level):单端口最高 2 Hz 重扫,端口数量多时扫描开销按端口数线性叠加。若预期会有大量流体端口(仓储墙),建议对同 tick 内多个端口的 dirty 做合并/节流。
  • small.png(small 字体图集)被重编码(palette+tRNS → RGBA;逐像素比对后字形掩码仅 1 个像素差异,位于字符 B,不透明像素 660→661):功能上无影响,但该图集被仓储/终端所有小字号数量文本共享,属不可 review 的噪声变更,建议还原(也不影响新功能——数字、BmB.%: 在既有图集里都已存在)。
  • 夹带的无关注释/清理PlayerSetting.addCustom(ItemStack)FilterCategory import 删除、ModelSelectionBakery 改为 record 模式匹配、TerminalJeiTransferSupportdeficit <= 0deficit == 0deficit = max(0, …),两者严格等价)。均已确认安全(addCustom(ItemStack) 无调用点),但建议拆出或在描述中列出。

🟢 看起来不错

  • 完全对齐 issue [TODO] 仓储流体端口 #4792 的规格(逐项核对见下表),配方 空/潜影壳/空 + 储罐×3 + 空/潜影壳/空 → 1 与 TODO 完全一致。
  • 伪槽位隔离做得系统FLUID_SLOT_BASE = 1<<24 独立编号段,并在所有物品路径显式跳过/规避——createItemOrder(取物)、resetServerSlotshasContentsapplyPreservedSyncResults、折叠显示 appendFluidSlots(只补 order 中存在的流体槽位,避免与分类黑名单冲突)、客户端 getStorageSlot 返回 null、服务端 sync/terminalTake 越界守卫。未发现越界或缺漏。
  • 交互按流体身份而非下标匹配(客户端随点击上报 icon,服务端 FluidRegistry.find(...)isSameFluidSameComponents 定位),正面处理了"点击与处理之间列表变化"的并发取错流体问题;装桶/倒桶的关键路径都做了先模拟后执行 + 分级回滚(空容器先扣、失败还回;流体先 simulate、EXECUTE 不足则灌回),原子性比一般实现稳。
  • returnItems 三级去向(主去处 → 次去处 → 掉落世界)修掉了"两处都放不下就吞物品"的旧坑;getContainerSize()Inventory.INVENTORY_SIZE(6 处)修掉了盔甲槽/副手槽被当背包用的老问题,且 hasEnoughMaterialtransferMaterialExact 的统计/扣取范围已同步。
  • 流体端口生命周期对称onLoadFluidNetworkManager.addContainersetRemovedremoveContainer + StorageFluidRegistry.unregister 成对;静态表 clear() 挂在 ServerLifecycleEventListener.onServerStoppedlivePortslevel.isLoaded(pos)getBlockEntity(无同步区块加载)。
  • 序列化完整saveAdditional/loadAdditionalTank 复合标签,与储罐同构,因而能直接复用 FluidTankItemTooltip)/getUpdateTag/getUpdatePacket 齐备;掉落物 saveToDrop 对空端口不写 NBT 以保证可堆叠,并同时处理 getDrops、中键克隆、创造模式破坏三条路径。
  • FluidTankRenderUtil 抽取保持等价insetPixels = 0inset == TANK_WmaxY = 1 - insetmaxY = inset + fill * height 与原实现逐项等价(已核对),气体分支不变;窗口 inset 由 WINDOW_INSET_PIXELS 常量在 BER 与物品渲染器间共享,水面在手上与世界里一致。
  • 资源/注册齐全:blockstate、block model(新增 render_type: cutout 使玻璃窗口透出流体)、item model、loot table、recipe + advancement、创造栏(FunctionalBlocksFunctionalBlocksSections 双处同步)、mineable/pickaxe tag、category/fluid.json 数据包条目、en_us/en_ud 对称均到位;渲染器在 ModBlockEntities.renderer(...) 注册,Capabilities.FluidHandler.BLOCK 已注册(CapabilitiesEventListener)。
  • 未发现调试语句、TODO/FIXME、硬编码凭证;EOF 缺换行 6 处全部来自 datagen 新建 JSON(预期,非异常量级)。

📋 声称验证表

声称(PR 描述 / issue #4792 状态 对应实现
resolved #4792 ⚠️ 功能已实现,但 issue 仍 open(PR 未合并,自动关闭属预期);规格逐项见下
配方:空/潜影壳/空、储罐×3、空/潜影壳/空 → 1 ShapedRecipeLoader.storageFluidPort + recipe/storage_fluid_port.json
可与集装箱/存储站连接,与仓储端口互相延伸 StoragePortBlockEntity.findSoleCore/isPort 静态化共用
128 B 容积、单一流体、拆除保留、海绵清除 ⚠️ CAPACITY_MB/FluidTank/saveToDrop/clearFluid(海绵清除残留 0 条目,见警告)
非气体流体:以等效高度把水位维持在 50%~75%,最远越快,最多 20 格 ADJUST_FILL_LOW/HIGH = 0.5/0.75ADJUST_RATEMAX_HEIGHT_BIAS = 20Math.clamp
气体行为类似(模拟压力) 偏置写入 FluidEndpointeffectiveHeight,网络以 effectiveHeight - Y 推气压(FluidNetworkScanner.heightBiasAt
UI 显示流体、不占类别、1 mB = 1 物品排序 addFluidEntriesentry.amount() 直接参与比较)+ 独立伪槽位
少于 1 B 用 mB、到 1 B 用 B、小数三位有效数字 FluidAmountUtil.formatAmountMathContext(3))/formatExactAmount
点击流体格自动用铁桶装一桶取出;缺桶弹提示 StorageInput.FLUID_BUCKET + fillBucketFromStorage + FluidNotice.BUCKET_MISSING 浮层
放入桶装流体时自动倾倒入同流体端口,无端口则存桶物品 pourIntoFluidPort + findAcceptor,左键倒/右键存桶
JEI 识别空桶+流体组合,自动盛装并填充合成表 produceFilledContainer + StorageJeiSupport.producibleFromFluid / addProducibleFluidContainers / TerminalJeiTransferSupport.producibleFromFluid
描述:增加了之前缺失的部分快捷键(Shift+双击批量) lastQuickMoved + findInventorySlotWith + isDoubleClick 分支
描述:修改了 JEI 转移前的清空物品去向 clearCrafting 改为存储优先(returnItems(..., true)
描述:修复物品被放入盔甲栏 6 处 getContainerSize()Inventory.INVENTORY_SIZE
描述未提及:分拣器朝向翻转 / ChuteBlockItem ⚠️ 见警告(属未声明的夹带变更)

🧪 测试建议

被测目标 推荐测试场景 优先级
StorageServerStub.terminalReorder + TerminalRemoteOverlay 接入流体端口后打开远程终端,确认页尾无空白格、翻页/光标不被伪槽位影响 🔴
StorageFluidPortBlockEntity.clearFluid 海绵清空后确认 StorageFluidRegistry.collect 不再返回 0 条目 🟡
StoragePortBlockEntity.findSoleCore 端口链跨未加载区块时不得触发同步加载(isLoaded 守卫回归) 🟡
StorageFluidRegistry.collect/drain/findAcceptor 多端口同流体合并、跨维度同坐标、端口拆/挂迁移后 drain(旧存储) 不得抽到新存储的流体 🔴
produceFilledContainer 回滚路径 空容器不足 / 流体不足 / 模拟通过但实际抽取失败 → 物品与流体总量守恒(不增不减) 🔴
StorageFluidPortBlockEntity.saveToDrop / getUpdateTag 满/半/空端口掉落物 NBT、空端口物品可堆叠、重载后流体与水位一致 🟡
FluidAmountUtil.formatAmount 0 / 999 / 1000 / 1.25 B / 1000 B(→1K B) / 128000 mB 边界 🟢
分类过滤 matchesFluidCategoryFilters 流体分类黑名单(折叠/非折叠两种显示路径)、@namespace#tag 搜索 🟡

结论: REQUEST_CHANGES — 功能主体质量高、与 issue #4792 规格高度吻合,越界/并发/序列化等硬风险都处理到位;但 terminalReorder 把流体伪槽位发给不支持的终端界面 是本 PR 引入的可见 UI 缺陷,需先修(改用 createItemOrder 即可);另请确认/拆分分拣器朝向与 pipe_glass_node.png 两项夹带变更,并处理海绵清除残留下 rememberedFluid 的问题。

建议 PR 标题gh 未认证,未自动修改):
Add Storage Fluid Port for storage networks 新增仓储流体端口


由 Hermes Agent 审查

- 添加 hasAdjacentPipe 方法判断端口是否有相邻管道连接
- 在 adjustHeightBias 方法前置条件判断无相邻管道时跳过调整
- 减少无管网连接时的重复 markDirty 调用,防止全局管网重建
- 引入 Level 和 Direction 检查相邻方块是否为管道部件
- 保持管网高度偏置调整仅在有效连接时执行,提高性能与稳定性
@Gugle2308

Copy link
Copy Markdown
Collaborator

我已完成对 PR #4806 的完整审查(diff 48 文件 / +3071 −155,PR 处于 draft、mergeable=clean)。以下是审查意见:


代码审查摘要 — PR #4806

操作: edited(draft;github 报告 mergeable: true / clean,无冲突)
范围: 48 个文件(36 Java / 14 新增 / 0 删除)/ 4722 行 diff
基线: fluidtank/1.21/1.6dev/1.21/1.6(1.21.x 常规 feature PR,已按约定跳过 26.1 迁移类检查)
说明: 本地无 Gradle 依赖缓存,未做编译验证(无 ./gradlew compileJava 结果),下述结论均来自源码交叉核对。

📋 声称验证表

PR 描述声称 状态 对应实现
新增仓储流体端口(方块/BE/物品/渲染/配方/语言) StorageFluidPortBlockStorageFluidPortBlockEntityStorageFluidRegistryStorageFluidPortBlockItemStorageFluidPortItemRenderer/BlockEntityRendererShapedRecipeLoader#storageFluidPort、生成资源 blockstate/model/loot/advancement/pickaxe tag/lang(en_us+en_ud) 齐全
resolved #4792(界面流体显示与交互、1 mB = 1 物品折算排序) addFluidEntries/createOrder(includeFluids)/FLUID_SLOT_BASEFluidCategory + ICategory#testFluidStorageScreen 流体伪槽位渲染与点击、FLUID_BUCKET 交互
增加了之前缺失的部分快捷键 Shift+双击批量移入(lastQuickMoved + findInventorySlotWith)、Alt+左键倾倒/右键存桶、TexturedButton 右键回调(存入按钮右键=不倾倒)
修改 JEI 转移前的清空去向 clearCrafting 改为 returnItems(..., true)=先入存储、放不下回退背包
修复物品被放入盔甲栏 placeCraftingResult/placeCraftingResultToInventory/transferMaterialExact/transferFromItems/giveBackToInventory/hasEnoughMaterial/countInInventory 统一改 Inventory.INVENTORY_SIZE(残留 4 处 getContainerSize() 为只读扫描,无影响)
(未声称)物品分配器行为变更 ⚠️ ItemSplitterBlock#getStateForPlacementgetOpposite() 改为 getHorizontalDirection()(默认朝向反转);ModBlocks.ITEM_SPLITTER.item() 改为 .item(ChuteBlockItem::new)(右键容器时不再打开容器界面,直接放置)

🔴 关键(需修复后合并)

  • rpc/StorageServerStub.java withdrawNeedsFromStorages(约 3225 行)— 背包满时现场盛装出的流体桶会被静默丢弃(流体+空桶已被消耗)
    int produced = produceFilledContainer(player, new StorageView(storages, List.of()), resource, required);
    if (produced > 0) {
        player.getInventory().add(resource.copyWithCount(produced));   // 返回值被忽略
    produceFilledContainer 已经实际扣掉空容器并抽走端口流体,而 Inventory#add 在放不下时返回 false 并把余量留在传入栈里——这里传入的是临时副本,余量直接消失。同函数下方物品路径恰恰为此做了防护(getInventorySpace + 注释「为防背包放不下导致 add 丢弃」),新分支破坏了该不变式。触发条件:JEI 快速合成补库需要流体桶、存储有空桶+对应流体、但主背包已无空间。
    建议:先 getInventorySpace 限制 produced(或 add 失败时 view.insert 兜底、最终 Block.popResource 掉落),与下方路径保持一致。

⚠️ 警告

  • integration/jei/StorageJeiSupport.java#hasFluidFor 的命名与 javadoc 与实现完全相反(潜在逻辑反转陷阱)
    注释写「仓储中存在足量(至少一桶)的同种流体」,实现却是:找到足量同种流体时 return false,不足时 return true。两处调用点(producibleFromFluidaddProducibleFluidContainers)都是按「true = 不可现场盛装/需跳过」来用的,因此当前行为是对的,但名字与文档是反的——这正是历史上被误改成真 bug 的高危模式。建议改名为 fluidMissingFor(或反转实现+同步调用点),并修正注释。

  • 流体数量格式化与全库既有实现不一致(同一数值在 tooltip 与槽位里显示不同)
    新增 FluidAmountUtil 自定规则(<1 B 用 mB;≥1 B 保留三位有效数字;≥1000 B 走 FormattingUtil.toAbbrNum1.5K B),而本方块物品的 tooltip 走 FluidTankItemTooltipClientFluidTankTooltip → 既有 UnitUtil.fluidUnit(两位小数截断、>1000 B 用 KB)。例如 1280 B:槽位显示 1.28K B,同物品 tooltip 显示 1.28 KB;1536 B 同理。仓储槽位 tooltip 用 formatExactAmount1.575 B)也与储罐 tooltip(1.57 B)不同。建议直接复用 UnitUtil.fluidUnit,或至少让规则与它对齐(多端口合计很容易超过 1000 B)。

  • api/tooltip/ItemTooltipManager.java 未登记 STORAGE_FLUID_PORT
    同类方块都有条目(STORAGE_PORTFLUID_TANKLARGE_FLUID_TANKCREATIVE_FLUID_TANK),新方块没有 NORMAL/SHIFT 描述(也就没有 tooltip.anvilcraft.item.storage_fluid_port 语言键)。该方块机制并不直观(128 B 单流体、水位自适应等效高度、门格海绵清除、拆除保留流体),建议补一条,与仓储端口对齐。

  • 两处未写进 PR 描述的行为变更,请确认是否有意为之(commit 里能看到意图,但 desc 未提)

    1. ItemSplitterBlock 默认朝向反转(本次为潜入反向),会改变已有玩家的放置手感(该方块在 dev 分支较新,影响面应可控);
    2. ITEM_SPLITTER 换用 ChuteBlockItem:右键容器面时改为「优先放置本体」,不再打开容器界面。
      建议在 PR 描述中补上这两点,避免被当成无意的顺手改动。

💡 建议

  • StorageFluidRegistry#find 增加空栈守卫。 客户端在流体条目失效(getFluidSlot 越界返回 null,如端口被拆/移除但客户端 order 未刷新)时会以 FluidStack.EMPTY 上报身份,服务端随即执行 FluidStack.isSameFluidSameComponents(entry.icon(), EMPTY)。本仓库其它调用点(ControlValveBlockEntity#allowsFluidPipeNetwork#isInfiniteGasSource)都在比较前先跳过空栈;建议 if (fluid.isEmpty()) return null;,不依赖空栈比较语义。
  • 流体伪槽位编号与 collect() 顺序耦合。 FLUID_SLOT_BASE + index 的 index 来自 StorageFluidRegistry.collect()(端口集合为 HashSet,迭代顺序随端口增删可能变化),而客户端的 orderreorder RPC)与 fluidssync RPC)是两次独立调用——两次之间端口集合若变化,同一槽位号可能指向另一种流体(图标/数量错位;交互本身按身份匹配,是安全的)。建议让 collect() 顺序确定化(按维度+坐标排序)或把流体条目与槽位号放在同一响应里下发。
  • RPC/网络契约已变更。 interact/deposit/moveSameToStorage 签名、SyncResult/InteractionResultStreamCodec 都改了(追加字段),客户端与服务端必须同时升级;StorageInput.FLUID_BUCKET 追加在枚举末尾(ordinal 稳定)👍。
  • heightBias 不持久化。rememberedFluid 一样只在运行时存在,重载后归零再自适应。rememberedFluid 已在 javadoc 明确说明,建议 heightBias 也加一句,避免后续被当成漏存字段。
  • 遗留资源(非本 PR 引入): models/block/hd_storage_port.jsonhd_storage_fluid_port.jsonstorage_port_consolidator.jsonhd_storage_port_consolidator.json 及对应贴图全库无引用(无同名方块/blockstate)。如果 HD/整合器变体是后续计划可以忽略,否则建议一并清理或注明。
  • StoragePortBlockEntity.findSoleCore 作为跨类使用的 public static 扫描器略显越界(流体端口现在也调它),后续可考虑抽到共享 helper。

🟢 看起来不错

  • 生命周期与静态表对称性完整: onLoad → FluidNetworkManager.addContainer / setRemoved → removeContainer + StorageFluidRegistry.unregisterServerLifecycleEventListener#onServerStopped 清理 StorageFluidRegistry(与 StorageServerStub.clear() 对齐),未发现泄漏或不对称。
  • 序列化/掉落与储罐完全同构: saveAdditionalgetUpdateTag(客户端渲染需要)、saveToDrop(空罐不写 NBT 以保证可堆叠,与 FluidTankBlockEntity/StoragePortBlockEntity 同写法)、collectComponents()CAPACITY_MB 与 tooltip 容量一致。
  • 流体交互的原子性处理到位: 先模拟(drain(..., true)hasEnoughContainers)再执行、先扣空容器后抽流体(失败只需还容器)、并发下抽不满时还容器+refillFluid 回灌;客户端上报流体身份copyWithAmount(BUCKET_VOLUME))而非下标,规避端口增删导致的错配;失败原因用 FluidNotice 回传并由界面浮层渲染(不用会被界面遮住的动作条)。
  • StorageScreen 浮层层级处理有据: FLYOUT_Z = 300 + 绘制前后 graphics.flush(),避开数量数字(z=200)与工具提示(z=400);点击处定位并夹在窗口内,且让开 TOOLTIP_TOP_OFFSET,注释写清了依据。
  • 交互路径一致性: 门格海绵清液与 LargeCauldronBlock 一致;onPlayerUse 的「先瓶子后桶」与 FluidTankBlockEntity 一致;流体分类 test() 恒 false、testFluid() 判定,NamespaceCategory/Or/AndtestFluid 覆写完整;CategoryEntry 默认 UNLIMITED,默认分类加入流体分类不会误过滤物品。
  • 生成资源完整且对称: 5 个新语言键在 en_us/en_ud 同时存在;ModCategoryTypes/ModCategories/数据文件 category/fluid.json 三处类型名一致;创造标签页两处(FunctionalBlocks + FunctionalBlocksSections)都已同步。
  • 服务端排序与客户端过滤分工清晰: 普通文本搜索在服务端一律放行(无客户端语言环境),由客户端 applySearchFilter 按本地化名与 id path 过滤;# 标签搜索对流体不放行(注释解释到位),createItemOrder 从源头保证取物路径不掺入伪槽位(避免 StorageView 越界)。

🧪 验证建议

仓库无 src/test,建议合并前至少:

  1. ./gradlew compileJava(我这边无法运行,未做编译验证);
  2. ./gradlew runData 并检查生成 diff(本 PR 手改了 en_us/en_ud/blockstate 等生成文件,需确认与 datagen 一致);
  3. 游戏内手测:空桶左键取液 / 满桶左键倾倒 / 右键存桶不倒、Shift+左键装桶入背包、Shift+右键(桶入库)、多端口合计跨 1000 B 的显示(对比物品 tooltip)、拆端口后旧流体槽位点击、Inventory 满时的 JEI 补库路径(对应上面 🔴)、潜影集装箱/超维存储站两种核心、端口与仓储端口互连延伸、门格海绵清液与拆除保留流体。

结论: COMMENT(draft 阶段,建议先修 🔴 项并补 PR 说明) — 整体实现质量较高(生命周期/序列化/原子性/身份匹配都很扎实),主要问题集中在 1 处物品丢弃风险、1 处命名与文档反向的高危可读性缺陷,以及若干未在描述中说明的行为变更与显示/文档一致性。

建议标题: Add Storage Fluid Port 添加仓储流体端口("Added" 改祈使式、去掉句点;格式 <英文> <中文>)。
本次未自动修改标题:gh auth status 显示 The token in /opt/data/home/.config/gh/hosts.yml is invalid(未认证),按流程不导出凭据,因此仅给出建议标题。


由 Hermes Agent 审查(基于 patch-diff.githubusercontent.com 公开 diff + 交叉核对 PR 分支源码 ce22079b9

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/hardcoded-block-list-coverage.md in skill 'minecraft-mod-pr-review' (1 replacement). · Patched references/shell-only-diff-navigation.md in skill 'github-webhook-pr-review' (1 replacement).

- findAcceptor 方法改为只返回已存放同种流体的端口,避免空端口误吞桶装流体
- 新增 findRefillTarget 方法,优先同种流体端口,无合适时选空端口,防止回滚流体丢失
- refillFluid 方法改为使用 findRefillTarget,保证灌回流体时端口匹配更准确
- 增加相关方法的详细注释说明使用场景和设计考量
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -s "https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806" | python3 -c "import sys,json; d=json.load(sys.stdin); print({k:d.get(k) for k in ['state','title','mergeable','mergeable_state',...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /opt/data/workspace/AnvilCraft; sed -n '3710,3730p' /tmp/stub_head.java; echo "=== issue 4792 ==="; curl -s "https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792" | head -c 1200; echo; ech...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

which convert magick python3 2>/dev/null; python3 -c "import PIL; print('PIL', PIL.__version__)" 2>&1 | head -2

Reason: script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ No activity for 15 min. If the agent does not respond soon, it will be timed out in 15 min. You can continue waiting or use /reset.

@Gugle2308

Copy link
Copy Markdown
Collaborator

我已完成审查(diff 已下载并交叉引用了目标分支与 PR head 源码)。以下是审查意见:


代码审查摘要 — PR #4806

操作: synchronize(PR 当前为 draft
范围: 48 个文件(36 Java + 12 资源/数据,其中新增 14、删除 0)/ diff 4776 行(API 统计 +3125 / −155)
合并状态: mergeable: truemergeable_state: clean
上游依据: 目标 issue #4792「[TODO] 仓储流体端口」——本次逐条对照其规格核对(见下方表格);该 issue 目前仍为 open,且 PR 描述用的是 "resolved" 而非 fixes/closes,合并后不会自动关闭 #4792,请补 closing keyword 或手动关闭。

✅ 与 #4792 规格的逐项核对

# #4792 规格 状态 实现位置
1 配方:空/潜影壳/空 + 储罐×3 + 空/潜影壳/空 → 1 ShapedRecipeLoader#storageFluidPort + 生成的 recipe/advancement
2 与仓储端口互连、可互相延伸 StoragePortBlockEntity#findSoleCore 抽为静态 + isPort() 同时识别两类端口
3 128 B 容积、单一流体、拆除保留、门格海绵清除 CAPACITY_MB = 128*1000FluidTanksaveToDropclearFluid()
4 非气体流体 + 接管道时按等效高度把水位维持 50%~75%,最多 20 米,偏离越远越快 adjustHeightBias/computeNextHeightBias(LOW .5 / HIGH .75 / MID .625 / RATE 8 / MAX 20)+ FluidNetworkScanner#heightBiasAt
5 气体同样用模拟压力维持 50%~75% 同一 heightBias 参与 effectiveHeight(气压推导),已作用于气体
6 UI 显示流体、不占类别、数量排序 1 mB ≈ 1 物品、<1B 用 mB / ≥1B 用 B / 3 位有效数字 StorageServerStub#addFluidEntriesFluidAmountUtilStorageScreen#renderFluidIcon
7 点击流体格用铁桶装一桶取出,缺桶弹提示 takeFluidBucket + FluidNotice.BUCKET_MISSING/NOT_ENOUGH(浮层提示而非动作栏,正确)
8 放桶装流体时自动倒入「有相同流体」的端口,没有则存桶物品 findAcceptor回退空端口)+ 调用方回退到 view.insert
9 JEI 填充合成识别「空桶 + 流体」组合并自动盛装 produceFilledContainer/countProducibleContainers + StorageJeiSupport/TerminalJeiTransferSupport 预检

⚠️ 警告(建议合并前处理)

  1. integration/.../TerminalRemoteOverlayterminalReorder 不匹配StorageServerStub#terminalReorder 现在返回含流体伪槽位的 order(createOrder 默认 includeFluids=true),但终端浮窗的内容缓存永远不会包含这些槽位:服务端 syncindex >= view.size() 的槽位是直接 continueStorageServerStub:203),不返回内容。后果有二:

    • syncFullContentsmissing 永不为空 → 普通文本搜索路径永远走不到 missing.isEmpty() 快路径,每个刷新周期都会多发一次 sync RPC(FULL_SYNC_PAGE 预算也被白耗);
    • 终端浮窗里流体不显示,且 terminalTake/terminalTakeToInventoryslot >= view.size() 直接 no-op(不崩溃,但点了没反应)。
      建议 terminalReorder 改用 createItemOrder(与 extractFromTerminal/terminalExtractFirst 一致),或在客户端过滤 >= FLUID_SLOT_BASE 的槽位。
  2. StorageJeiSupport#hasFluidFor 语义与方法名完全相反 — 该方法是「流体容器且储量不足」时返回 truecontent.isEmpty()true;找到了足量同种流体 → false),两个调用点都是按「true = 不可盛装、跳过」在用(producibleFromFluid 直接 return falseaddProducibleFluidContainers 直接 continue)。当前行为正确,但命名反向是典型维护陷阱(后人「修正」返回值就会静默废掉整个 JEI 流体合成)。建议改名为 cannotFillFromFluid/isFluidMissing,或反转返回值使名字与语义一致。

  3. 本 PR 夹带与流体端口无关的行为变更(ItemSplitterBlock + ITEM_SPLITTER 物品类型)getStateForPlacementgetHorizontalDirection().getOpposite() 改为 getHorizontalDirection(),即默认朝向整体反转(潜行分支随之互换);同时 ModBlocks.ITEM_SPLITTER.item() 改为 .item(ChuteBlockItem::new),使其获得 onItemUseFirst 的「对着有物品能力的方块时优先放置」行为。这两处会改变既有玩家的摆放习惯与放置优先级,PR 描述完全未提及。请说明是否有意;若非本功能必需,建议拆到独立 PR 以便回溯。

💡 建议(非阻塞)

  • block/item/StorageFluidPortBlockItemhd_storage_fluid_port.json 渲染类型不一致 — 本 PR 给 models/block/storage_fluid_port.json 补了 "render_type": "minecraft:cutout",但 HD 变体 models/block/hd_storage_fluid_port.json 未同步(该模型仓库内无引用、由外部 HD 资源包覆盖使用)。若 HD 包路径会被启用,玻璃窗口渲染会与普通模型不一致。
  • StorageFluidRegistry#collect 的伪槽位编号依赖 HashSet 迭代顺序 — 内层是 Set<BlockPos>HashSet),livePorts 又用 Set.copyOf(positions)collect 按「端口首次出现顺序」决定流体下标,而客户端展示的 orderreorder RPC)与 fluids 列表(sync RPC)来自两次独立调用。端口集合变动时两次调用的顺序可能不同,界面会把 A 流体的数量画在 B 流体的格子里(交互按流体身份匹配,故只是短暂显示错位、下次刷新自愈)。建议端口按「维度 + 坐标」排序(或对结果按流体 id 排序)使编号确定化。
  • pourIntoFluidPort 的 JavaDoc 与实现不符 — 文档写「只有存在能接收该流体的端口(同种流体或空端口)时才会倾倒」,但 findAcceptor 明确回退空端口(这正是 [TODO] 仓储流体端口 #4792 的规则,方法体内的注释是对的)。请把外层 JavaDoc 改成与实现一致,避免后人按文档「修复」出吞桶 bug。
  • client/selection/ModelSelectionBakery 的 record 解构重写instanceof ModelSelection.Fixed fixedFixed(SelectionPart part),含 part1 改名)属纯风格重构,与本功能无关,建议回退以缩小 diff、降低冲突面。
  • 流体格 tooltip 与物品格条件不一致 — 物品格仅在 this.carried.isEmpty() 时才设置 renderingTooltips,流体格无条件设置(StorageScreen:944),指针上拿着物品悬停流体格时会多出一层 tooltip。
  • StorageFluidPortBlockItem#getTooltipImage 恒返回非空 TooltipComponent(空罐也显示液面框);与 FluidTankBlockItem 行为一致,故仅提示确认是否有意。
  • 建议 PR 描述补充「为什么改 pipe_glass_node.png(管道玻璃节点共用贴图)与 font/small.png(SMALL_FONT 被仓储 UI 等复用)」——两张均为共享资源,二进制 diff 无法在评审中核对,属全模组视觉回归面。

🟢 看起来不错

  • consumeCraftingInput 重写后忠实对齐原版 ResultSlot.onTake:剩余物与原物同种(催化剂)时 count 净变化为 0 → changed=false 终止循环(防无限产出);剩余物不同种且槽内还有余料时归还玩家/存储并保留余料(原版正是 inventory.add/drop);槽位刚好清空时剩余物落回原槽。三种情形的判定链清晰且有注释。
  • **物品三级归还 returnItems(主去处 → 次去处 → Block.popResource 兜底)**消灭了「两处都放不下就静默吞物品」的老问题;JEI 转移前的 clearCrafting 改为「先进存储、退回背包」也符合描述。
  • 盔甲槽修复到位placeCraftingResult*transferMaterialExacttransferFromInventorygiveBackToInventoryhasEnoughMaterialcountInInventory 均改为 Inventory.INVENTORY_SIZE;同时正确地保留restockHandgetContainerSize() 上界判定(副手槽 = 40,若也改成 INVENTORY_SIZE 会把物品均衡的副手补货弄坏),这个取舍很细。
  • 流体取出的失败原子性fillBucketFromStorage 先模拟、后扣桶、再 drain 并校验实际抽出量,不足则 giveEmptiedContainer 还桶;produceFilledContainer 先模拟确认流体+空桶、再扣桶、最后 drain 并校验,失败走 giveBackContainers + refillFluid 回灌。提示优先级(先判缺桶、再判储量)也符合直觉。
  • 服务端越界防护与「按身份而非下标」定位sync 跳过越界/重复索引、takeFluidBucket/fillBucketFromStorageisSameFluidSameComponents 匹配(客户端随点击上报流体身份),interactWithStorage>= FLUID_SLOT_BASE 直传逻辑槽位——这些设计比按下标访问稳健得多。
  • 端口水位调节加了 hasAdjacentPipe() 前置守卫(无管网时不空转、不 markDirty)、偏置在目标区间内保持不动(防止在「吸入/排出」间反复横跳)——两处都写明了理由。
  • 客户端 7 处(renderStorageContents/applySearchFilter/appendFluidSlots/applyPreservedSyncResults/resetServerSlots/hasContents/getStorageSlot)与两端 StorageFluidRegistry.FLUID_SLOT_BASE 判定齐全,伪槽位未进入 contents/serverSlots/logicalSlots 等物品语义容器;rebuildFoldedGroupscontents.getOrDefault 故折叠模式不会 NPE。
  • StorageFluidRegistry 生命周期对称(clear() 挂到 ServerStoppedEventsetRemoved 注销、validateLink 先清旧存储条目再重登记)、livePortsisLoaded 再取 BE,静态表泄漏面控制得好。

📋 声称验证表(PR 描述)

声称 状态 对应文件
Added Storage Fluid Port StorageFluidPortBlock/BlockEntity/BlockItemStorageFluidRegistry、渲染器、注册、配方/掉落/标签/语言
resolved #4792 ⚠️ 功能逐条符合规格,但 issue 仍 open 且无 closing keyword,合并不会自动关闭
增加了之前缺失的部分快捷键 Shift+双击左键批量移入(lastQuickMoved/findInventorySlotWith)、Alt+左键倾倒 / Alt+右键存桶(moveSameToStorage(slot, pour))、存入按钮右键存入(TexturedButton 右键支持 + deposit(..., pour=false)
修改了 JEI 转移前的清空物品去向 clearCraftingreturnItems(..., storageFirst=true)returnToInventoryOrStorage 删除、替换为三级归还
修复了会把物品放入盔甲栏的问题 6 处 getContainerSize()Inventory.INVENTORY_SIZE(并保留 restockHand 所需的上界)

🧪 测试建议

被测目标 推荐场景 优先级
StorageFluidPortBlockEntity#computeNextHeightBias 49.9%/50%/75%/75.1% 边界、空罐归零、±20 clamp、无相邻管道不调整 🔴
StorageFluidRegistry 同种流体跨端口合并;取空后 0 占位与顺序稳定;drain simulate/execute 一致;跨维度同坐标隔离;register/unregister 对称 🔴
produceFilledContainer / fillBucketFromStorage 模拟通过但执行失败的回滚(不吞桶、不凭空增流体);空桶来自指针/背包/存储三条路径;指针被占用时不消耗桶 🔴
掉落 NBT 往返 满/空端口拆下→重放的 Tank/Fluid 一致性;空端口不加 BE data(保证与原物品堆叠) 🟡
FluidAmountUtil 999/1000/1250/1_000_000 mB 与 3 位有效数字、K/M 缩写阈值 🟡
RPC 契约 interact 新增 FluidStack(空栈编解码)、SyncResult/InteractionResult 新增字段的编解码与越界槽位容错 🟡
背包遍历回归 背包满/副手槽(40) 物品均衡补货、JEI 取料不得动盔甲槽 🟡
StorageScreen 流体格交互 左键(空指针装桶 / 拿桶倾倒)、右键(存桶物品)、Shift+左键(直接入背包)与缺桶提示浮层 🟡

说明:本次为纯 diff + 上游源码交叉引用审查,未运行 Gradle 编译/runData(无 dev 构建环境),也未核对两张二进制贴图的实际像素变化;建议合并前由 CI 或作者补一次 compileJava + 游戏内验证。

结论

COMMENT — 未发现崩溃、吞物品或存档不兼容类阻塞缺陷,实现与 #4792 规格逐条吻合、失败回滚与越界防护做得比一般 PR 细。建议优先处理 ⚠️1(终端伪槽位反复同步/流体不可见)与 ⚠️2(hasFluidFor 反向命名),并说明 ⚠️3(ItemSplitterBlock 朝向反转等夹带变更)是否有意,之后即可从 draft 转 ready for review。

关于标题:gh auth status 显示 token 已失效(The token in .../hosts.yml is invalid),本次未自动修改标题。建议标题(已体现本 PR 实际范围): Add Storage Fluid Port with storage UI & JEI fluid support 添加仓储流体端口(含仓储界面与 JEI 流体支持)


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806

操作: synchronize(PR 当前为 draft
范围: 48 个文件(36 Java,14 新增 / 0 删除)/ +3125 −155;其中 8 个为生成资源
分支: fluidtank/1.21/1.6dev/1.21/1.6
核对基线:git fetchdev/1.21/1.6(32da04af1)与 PR head(d7a91a2cb)做交叉验证,非本地旧克隆


📋 声称验证表

声称 状态 证据
resolved #4792 ✅ 实现完整;#4792 仍 open 见下方 TODO 规格对照;注意目标分支不是默认分支,合并不会自动关闭 #4792,需手动 close
增加了之前缺失的部分快捷键 ⚠️ 部分可核实 新增:Shift+双击批量移入(lastQuickMoved+findInventorySlotWith)、Alt+左/右键(倾倒/普通入库)、存入按钮右键(pour=false)、Shift+右键入库。未见其它新增按键路径,建议在描述里逐条列出
修改了 jei 转移前的清空物品去向 clearCrafting 由「背包优先」改为「存储优先」,并抽出 returnItems(storageFirst) 三级去向
修复了会把物品放入盔甲栏的问题 8 处 inventory.getContainerSize()Inventory.INVENTORY_SIZE(placeCraftingResult*、giveBackToInventory、hasEnoughMaterial、transferMaterialExact、transferFromInventory×2、countInInventory)

✅ 与 TODO #4792 规格逐条对照(issue 正文即为验收标准)

TODO 要求 实现 状态
配方:潜影壳/储罐×3/潜影壳 → 1 个 ShapedRecipeLoader.storageFluidPort,与 issue 图案逐字一致
可与集装箱/存储站连接、与仓储端口互相延伸 StoragePortBlockEntity.findSoleCore 抽取 + isPort(同时识别两种端口)
128 B 容积、仅存单一流体 CAPACITY_MB = 128*1000 + FluidTank 单流体语义
拆除保留流体 saveToDrop(空罐不写 BE 数据,保持可堆叠)+ getDrops/playerWillDestroy
门格海绵右键清除 useItemOnclearFluid()
以等效高度把水位维持在 50%~75%,最多 20 格 adjustHeightBias/computeNextHeightBias(0.5/0.75/±20),且只在相邻管道时调整
气体行为类似(模拟压力) 偏置同时进 heightBiasAteffectiveHeight(气体气压由 effectiveHeight - Y 推导)

⚠️ 警告(建议修复)

  1. StorageServerStub.StorageFluidPortBlockEntity.validateLink 改变归属时不刷新存储版本 —— 端口新挂上存储或脱挂时只做 register/unregister,没有 StorageServerStub.onContentsChanged(storageId),因此 stub.orders 缓存不会被清空。而伪槽位编号(FLUID_SLOT_BASE + collect() 下标)是在 createOrder/addFluidEntries 时写进缓存的:新接入的端口流体在「下一次物品内容变化之前」不会出现在界面;端口被拆后其伪槽位也可能残留成空格子(点击发送空流体 → 服务端静默不动)。onContentsChanged 已会 version++/orderVersion/orders.clear(),建议在 validateLink 里判定 storageId 发生变化(含由非空变 null)时直接调用它。

  2. StorageJeiSupport.hasFluidFor() 的名字与实现相反 —— 实现在「非流体容器/仓储流体不足」时返回 true,「够量」时返回 false(javadoc 描述又与实现相反)。两处调用点(producibleFromFluidaddProducibleFluidContainers)目前逻辑是对的,但这种反向命名极易在后续改动中被当成 bug「修正」而反转语义。建议改名(如 lacksFluidForneedsProduction)或把返回语义倒过来。

  3. StorageServerStub.quickMoveToStorage 把「倾倒掉的桶」计成已入库 —— moveInventoryStackToStorage(..., pour=true) 返回的是倾倒的桶数,这里 moved.merge(key, inserted, …) 就把它记成「桶已进存储」,于是撤销记录(recordUndoextractByResource)会去存储里找并不存在的成品桶,撤销时该条目静默少还原。moveSameToStorage/deposit 的倾倒分支都显式计入 moved,此处与它们不一致,建议同样跳过。

  4. 与本 PR 主题无关的行为变更混在一起(建议拆 PR 或至少写进描述):

    • ItemSplitterBlock.getStateForPlacement:默认朝向由「getHorizontalDirection().getOpposite()(输出朝玩家)」翻转为「朝玩家视角方向(输出背向玩家)」,潜行语义也跟着反过来。这会影响所有新放置的物品分配器(与 BlockComparatorBlock 的约定一致,但与自身旧行为相反),需要明确这是修正还是变更。
    • ModBlocks.ITEM_SPLITTER 从默认 BlockItem 改为 ChuteBlockItem(可贴在容器上放置)。
    • ModelSelectionBakery 记录模式(Fixed(SelectionPart part))改造、PlayerSetting.addCustom(ItemStack) 删除(已核查:全仓库无其它调用者,删除安全)。
    • 贴图改动未在描述中说明:font/small.png调色板(类型 3) 改成 RGBA(类型 6)pipe_glass_node.png 也改像素。两张都是全局共享资源(前者影响全模组数量文本渲染)。已核对:尺寸未变(39×42 / 16×16),font/small.json 字形表未变,故不会破坏字形映射——但请确认是有意为之。

💡 建议(非阻塞)

  • StorageScreen.renderStorageContents 流体格 tooltip 缺 carried.isEmpty() 守卫:物品格路径在 hovered && this.carried.isEmpty() 才设 renderingTooltips,流体格无条件设置,指针上拿着物品时仍会弹流体 tooltip,与物品行为不一致。
  • TerminalJeiTransferSupport.collectMissingdeficit <= 0deficit == 0 是等价改写(上一行 Math.max(0, required - have) 已保证非负),无行为变化。若本意是「把可现场盛装的桶计入可得量」,containerByUid 并未统计它们——目前靠 withdrawNeedsFromStorages 新增的 produceFilledContainer 兜住,语义上能跑通,但注释/意图建议写清。
  • 门格海绵清除流体时没有音效;LargeCauldronBlock 同类路径会播 SoundEvents.SPONGE_ABSORB,建议对齐。
  • heightBiasrememberedFluid 都不持久化:前者重载后归零且 10 tick 内自愈(可接受),但 rememberedFluid 的"取空后仍显示 0 占位"语义在门格海绵清除后同样成立——条目要等一次排序失效才消失,属于预期内可忽略。
  • hasAdjacentPipe() 只判断「相邻是否有管道」,TODO 措辞是「通过管道连接了其他流体储存方块」;鉴于偏置在无相邻管道时根本不会被读取,当前实现作为优化是合理的,注释已说明,无需改。
  • TexturedButton 新增了 3 个重载,其中 (…, OnPress, OnPress)(…, OnPress, Component) 在传 null 时会有歧义,可考虑收敛为单个构造器 + 静态工厂。

🟢 看起来不错

  • 生命周期完整对称onLoad/addContainersetRemoved/removeContainer + StorageFluidRegistry.unregisterStorageFluidRegistry.clear() 已挂到 ServerLifecycleEventListener.onServerStoppedlivePorts 过滤未加载区块并用 Set.copyOf 防并发修改。
  • findSoleCore 抽取质量高:静态化后双端口共用,isPort 双向覆盖「流体端口延伸仓储端口链」与反向;端口侧 validateLink 先清旧存储登记再重登记,避免 A/B 存储串台与 drain(A) 抽走属于 B 的流体。
  • 伪槽位防护在所有路径上都考虑到了createItemOrder 专供取物路径(避免 StorageView 越界)、resetServerSlots/hasContents/applyPreservedSyncResults/applySearchFilter/getStorageSlot 全部跳过或按流体语义处理;服务端 sync 的越界守卫(base 已有)能安全忽略伪槽位,不会崩。
  • saveToDrop/getDrops/getCloneItemStack/playerWillDestroyStoragePortBlock 完全同构(含 Screen.hasControlDown() 中键克隆,已有同类先例,非新增风险);Capabilities.FluidHandler.BLOCK 注册进现成的 IFluidHandlerHolder 列表,IFluidHandlerHolder 仅需 getFluidHandler()
  • 渲染侧 API 使用均有先例StorageFluidPortItemRendererFluidTankItemRenderer 的忠实复刻;graphics.blit(x,y,0,16,16,sprite,r,g,b,1f)ControlValveScreen 写法一致;FluidTankRenderUtil 新增 insetPixels 走参数化而非改常量,默认路径行为不变。
  • Inventory.INVENTORY_SIZE 修复彻底:连回滚路径(returnItems 三级去向 + 掉落兜底)也一并处理,不会因为「两处都放不下」而吞物品;restockHand 保留 getContainerSize() 是有意为之(要写副手槽),非漏改。
  • consumeCraftingInput 的剩余物逻辑与原版 ResultSlot.onTake 对齐(槽空则剩余物留槽、槽仍有料则交还玩家/存储),并用「净变化为 0」判定催化剂配方未消耗,无限产出防护得以保留。

🧪 测试建议

被测目标 推荐场景 优先级
validateLink / 存储归属 端口从 A 存储改挂 B、拆掉核心、区块卸载后重载:UI 归属与 drain 目标不串台 🔴
produceFilledContainer 回滚 模拟通过但实际抽取不足、空容器扣取失败:桶与流体都不增减 🔴
takeFluidBucket / fillBucketFromStorage 储量 <1000 mB 无桶(提示「需要空桶」优先)、有桶不足量、指针被占用 🔴
pourIntoFluidPort 多桶整批倾倒、端口容量不足中途 break、空容器去向(入库优先→背包→掉落) 🟡
伪槽位编号稳定性 某流体被取空后槽位编号不前移;新增/移除端口后条目正确增删(对应警告 1) 🟡
clearCrafting 去向 存储放得下/放不下、toStorage=false 时背包优先 🟡
Inventory.INVENTORY_SIZE 边界 背包仅剩盔甲槽空位时产物不得写入盔甲/副手 🟡
高度偏置 50%~75% 收敛、边界不来回横跳、±20 截断、气体同样生效 🟢

结论: COMMENT(暂无阻塞性问题,但建议先解决警告 1、3 并把无关变更拆分/写明) —— 主体实现与 TODO #4792 高度吻合,注册/序列化/RPC/渲染/数据生成齐全,伪槽位与生命周期处理细致;主要问题集中在「界面刷新时机」「反向命名的可维护性」与「混入的无关行为变更」。PR 仍为 draft,转入 ready-for-review 时我可以再过一遍。

建议标题gh 的 token 已失效,本次未自动修改,请手动或修复 gh 认证后执行):

gh pr edit 4806 --title "Add Storage Fluid Port with fluid slots and bucket pour/fill interactions 添加仓储流体端口并支持流体存取交互"

由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched references/shell-only-diff-navigation.md in skill 'github-webhook-pr-review' (1 replacement). · Patched SKILL.md in skill 'pr-fix-claim-verification' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched SKILL.md in skill 'github-webhook-pr-review' (1 replacement).

- 移除端口时通知 StorageServerStub 清理相关归属缓存,避免伪槽位遗留空格子
- 更新注册逻辑,切换存储归属时注销旧归属并注册新归属,修正UI显示异常问题
- 增加归属变更时对旧归属和新归属均调用内容变化通知,确保界面正确刷新
- 修正 JEI 集成中流体容器判断逻辑,改为判断仓储中是否缺少足量流体以决定是否需要现场盛装
- 优化缺少流体判定函数注释,明确三种返回 true 的情况及 false 的语义
@PigeonNian
PigeonNian marked this pull request as ready for review September 12, 2026 02:57
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -sL "https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806" | python3 -c "import json,sys; d=json.load(sys.stdin); print('TITLE:',d['title']); print('BODY:'); print(d['body']); print('MERG...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/pulls/4806 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

curl -s "https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['body']);print('STATE:',d['state']);print('comments:',d['comments']...

Reason: Security scan — [HIGH] Pipe to interpreter: curl | python3: Command pipes output from 'curl' directly to interpreter 'python3'. Downloaded content will be executed without inspection.
Safer: tirith run https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792 — or: vet https://api.github.com/repos/Anvil-Dev/AnvilCraft/issues/4792 (https://getvet.sh); script execution via -e/-c flag

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806

操作: ready_for_review — 按要求只报实质问题,省略风格类建议
范围: 48 个文件(36 Java、14 新增、0 删除)/ 4806 行 diff(+3155 / −155)
CI: buildcheckstyle ✅ · mergeable_state: unstable(仅状态未全绿)
交叉验证方式: patch-diff + 本地 git show forkpr4806/head / upstream/dev12116 双分支核对(head 落后 dev,故对每个可疑 hunk 都区分了「PR 改动」与「dev 侧漂移」)


🔴 关键

1. 「只遍历主背包」的修复只做了服务端一半 —— 客户端 JEI 判定范围仍是含盔甲/副手的 0..40,会出现「JEI 认为可转移、服务端静默不转移」

本 PR 把服务端所有背包遍历由 inventory.getContainerSize() 收窄为 Inventory.INVENTORY_SIZE

  • StorageServerStub.hasEnoughMaterial(L2392)、transferMaterialExact(L2444)、countInInventory(L3426)、consumeOne/consumeEmptyContainergiveBackToInventory(L2751)、placeCraftingResult* —— 取用侧与放置侧都改成了 0..35。

客户端 JEI 的可用量统计没有同步收窄,仍包含盔甲槽 36–39 与副手槽 40:

  • integration/jei/StorageJeiSupport.java:252 availableCount()player.getInventory().getContainerSize()
  • integration/jei/transfer/TerminalJeiTransferSupport.java:384 collectAvailable() → 遍历 container.slots(JEI 的容器槽同样含盔甲/副手)

availableCount() 正是 checkMissingInputs()(JEI 检查阶段)判定「材料是否充足」的依据,检查通过后由服务端 transferMaterialExact 实际取料。于是:

  • 材料(或空桶)只存在于副手/盔甲槽时,JEI 判定 available > used → 检查通过 → 服务端 hasEnoughMaterial 为 false → transferMaterialExact 返回 0 → 转移静默失败,无任何提示,且合成格不会拿到材料。
  • 流体路径同理:TerminalJeiTransferSupport.producibleFromFluidavailableByUid(含副手)判定「有空桶」,服务端 countInInventory/consumeOne(0..35)找不到 → produceFilledContainer 返回 0,需求缺料。

改动前两端都用 getContainerSize(),范围是一致的(一致地有「会动盔甲栏」的 bug);现在只改一端,反而引入了范围不一致。

建议:availableCount()collectAvailable() 的统计范围一并收窄到主背包(或抽一个共享常量/工具方法,让「判定范围 == 取用范围」在代码层面强制成立)。若刻意保留副手可参与,则服务端取用范围也应包含副手。除副手/盔甲外,还有一种更隐蔽的等价缺口:StorageJeiSupport 计入合成格(①②)内物品,而服务端取用前会先 clearCrafting 归还——这一条已有注释说明,可不改。


⚠️ 警告

2. 流体伪槽位编号依赖 HashMap/HashSet 迭代顺序,同一流体的槽位号可能在会话中漂移

block/entity/storage/StorageFluidRegistry.java:105 collect() 的顺序来自 PORTSHashMap<ResourceKey<Level>, …>)与内部 HashSet<BlockPos> 的迭代顺序;而 StorageFluidPortBlockEntity.validateLink() 每 20 tick 就 unregister + register 一次(L229/L268-271),集合的插入/删除历史会变化,桶链重排后迭代顺序可能改变 → FLUID_SLOT_BASE + index 对应的流体在会话中可能换位,界面条目顺序会跳动。

正确性上已经有兜底(客户端点击上报的是流体身份而非下标,服务端 find()isSameFluidSameComponents 匹配),所以不会取错流体;但 collect() 的注释本身依赖下标稳定(「否则点击会指到别的流体」),且服务端 createOrdersync 是两次独立的 collect() 调用,下标不一致时图标会短暂错位。

建议: collect() 返回前按稳定键排序(维度 → BlockPos 坐标 → 流体 id)后再分配下标,使服务端 order 与客户端 fluids 下标可复现、跨调用一致。

3. 每次流体数量变化都清空排序缓存并触发整存储重同步(性能)

StorageFluidPortBlockEntityonContentsChanged(L71-81)除了 setChanged + sendBlockUpdated,还调用 StorageServerStub.onContentsChanged(storageId);后者会 version++、清空 order 缓存并对所有观察者触发重新排序 + 分块 sync RPC(StorageServerStub.java:3714)。而接入管网的端口在「水位自调 + 管网分配」下会持续进出液(水道机制本身就要求它在 50%~75% 区间反复交换),也就是可能每 tick 触发一次;对照物品端口 StoragePortBlockEntity.onContentsChanged(只 setChanged + sendBlockUpdated)没有这层通知。

建议: 对通知做合并/节流,例如最多每 N tick 通知一次、或数量变化超过阈值才通知、或仅在 STUBS 中确实存在该存储的观察者时通知。FluidNetworkManager.IDLE_INTERVAL(20)这类既有节流思路可复用。

4. 两处改动超出 PR 描述范围,改的是刚合入的 #4798 行为

  • init/block/ModBlocks.javaITEM_SPLITTER.item().item(ChuteBlockItem::new)ChuteBlockItem.onItemUseFirst 会直接在容器上放置方块,交互语义变化)
  • block/ItemSplitterBlock.java:56-59:默认朝向由 getHorizontalDirection().getOpposite() 改为 getHorizontalDirection(),即默认/潜行朝向整体互换(dev 侧仍是 getOpposite(),确认是 PR 侧改动)

两处都与仓储流体端口无关,也未出现在 PR 描述中。若是有意为之(提交信息「修正物品分拣器默认面朝」表明是刻意的),建议在描述里补一句或拆成独立 PR —— #4798 刚合入,同一版本内翻转朝向会让玩家困惑。


💡 建议

  1. 注释与实现矛盾StorageServerStub.pourIntoFluidPort 的 javadoc 写「只有存在能接收该流体的端口(同种流体或空端口)时才会倾倒」,但 StorageFluidRegistry.findAcceptor(L212)明确不回退到空端口(按 [TODO] 仓储流体端口 #4792 规格:只倒入有相同流体的端口,否则桶作为普通物品入库)。实现符合规格,注释需要改。
  2. zh_cn 未同步新 lang 键:本 PR 只加了 CategoryLang(en_us / en_ud 的 datagen 键)。src/main/resources/assets/anvilcraft/lang/zh_cn.json 是手工维护且惯例上会同步(如 screen.anvilcraft.storage.countcapacity.infinity 均有中文),缺失 block.anvilcraft.storage_fluid_portcategory.anvilcraft.fluidscreen.anvilcraft.storage.fluid_amount…fluid.bucket_missing…fluid.not_enough 共 5 个键 → 中文环境会落到英文。
  3. TerminalJeiTransferSupport.java:284deficit <= 0deficit == 0 是等价改写(上一行 Math.max(0, required - have) 保证非负),无行为变化;提交信息声称的「修正流体检测判定错误」实际不来自这里,可忽略。
  4. StoragePortBlockEntity.findSoleCore 对邻居使用 level.getBlockState() / level.getBlockEntity() 未做 isLoaded 校验(理论上会同步加载区块,C2ME 场景有死锁风险)。这是从物品端口沿用下来的既有写法,但流体端口现在也每 20 tick 走同一条路径,建议顺手加 level.isLoaded(neighbor) 守卫(FluidNetworkScannerhasAdjacentPipe 都已有该守卫,风格上更统一)。

🟢 看起来不错

  • 完整覆盖 [TODO] 仓储流体端口 #4792 规格:配方(空/潜影壳/空 + 储罐×3)、与仓储端口互相延伸、128 B 单流体、拆除保留流体、门格海绵右键清除、非气体流体 50%~75% 水位自调(上限 20 格)、1 mB = 1 物品排序、单位格式(<1 B 用 mB,≥1 B 用 B,三位有效数字)、点击装桶 + 缺桶提示、同流体自动倾倒、JEI 空桶 + 流体现场盛装 —— 逐条对得上 issue 文本。
  • RPC 设计稳妥interact 携带流体身份FluidStack.OPTIONAL_STREAM_CODEC)而非下标,避免列表变化时取错流体;并在分支执行后重读 player.containerMenu.getCarried(),修掉了动作前旧数量回传的问题。
  • 回滚链路考虑周到produceFilledContainer 先模拟(流体可抽 + 容器可扣)再执行,失败分别回滚容器(giveBackContainers)与流体(refillFluid,用允许空端口的 findRefillTarget);fillBucketFromStorage 抽液失败会把空桶还回;归还统一为「存储 → 背包 → 掉落」三级兜底,避免吞物品。
  • 合成剩余物重写与 ResultSlot.onTake 语义一致(空槽落剩余物 / 同种合并 / 否则归还),并由「内容或数量变化」判定是否算消耗,催化剂类配方不会无限产出。
  • 伪槽位过滤点齐全getStorageSlotresetServerSlotshasContentsapplyPreservedSyncResultsapplySearchFilter 都做了处理,且 createItemOrder(view, options, categories) 从源头把伪槽位排除在服务端取物路径之外(避免越界),比逐个调用方记得过滤更稳。
  • copyWithCount / ItemStack.isSameItemSameComponents 等都用的是副本emptyContainerOf / contentOf 不会污染真实物品堆。
  • 客户端渲染:renderSlotCount 抽公共方法后物品与流体数量字号/位置一致;graphics.flush() 的 z 序处理(浮层 300 / tooltip 400)有依据。
  • CI build + checkstyle 均通过,编译面(Math.clampjavadoc 引用、record pattern、@CallableParam 字段名)无隐患。

📋 声称验证表

声称 状态 对应证据
resolved #4792 #4792[TODO] 仓储流体端口 的 10 条要求逐条实现(见上);issue 当前仍 open,但 base = dev/1.21/1.6 即仓库默认分支,resolved #4792 是合法关闭关键字 → 合并后自动关闭
增加了之前缺失的部分快捷键 Shift+双击批量移入(lastQuickMoved + findInventorySlotWith)、Alt+左/右键区分倾倒与入库、流体格左/右键与 Shift 分支、TexturedButton 右键回调
修改了 jei 转移前的清空物品去向 clearCraftingreturnItems(..., storageFirst=true),先入存储再回退背包,兜底掉落
修复了会把物品放入盔甲栏的问题 ⚠️ 部分 服务端取用/放置路径已收窄到 INVENTORY_SIZE ✅;客户端 JEI 判定范围未同步 → 见 🔴1

标题建议(未自动修改:gh auth status 显示 /opt/data/home/.config/gh/hosts.yml token 失效,按流程跳过 gh pr edit

当前标题 Added Storage Fluid Port. 添加了仓储流体端口 只覆盖了新增方块,未体现本 PR 的仓储界面流体槽位、桶交互快捷键、JEI 现场盛装与几处修复。建议:

Add Storage Fluid Port with storage UI fluid slots and bucket shortcuts 添加仓储流体端口、仓储界面流体槽位与桶交互

🧪 测试建议

被测目标 推荐场景 优先级
StorageJeiSupport.availableCount / StorageServerStub.hasEnoughMaterial 材料仅在副手/盔甲槽时,JEI 检查与服务端取用范围必须一致(当前会静默失败) 🔴
StorageFluidRegistry.collect 多端口多流体 + 反复 register/unregister 后,下标与流体映射保持稳定 🟡
StorageServerStub.produceFilledContainer 模拟阶段通过、实际抽取不足(并发)时容器与流体双向回滚,总量守恒 🟡
StorageServerStub.consumeCraftingInput 催化剂类配方(剩余物 == 原料)净变化为 0 → 终止循环不漏产出 🟡
StorageFluidPortBlockEntity.computeNextHeightBias fill 0 / 0.49 / 0.5 / 0.75 / 1.0 边界与 ±20 钳制;空罐 → 0 🟡
StorageFluidPortBlockEntity.saveToDrop / loadAdditional 空端口拆下不与未放置物品堆叠;带流体放置后恢复数量 🟡
StorageScreen.getFluidSlotAt / getFluidSlot 伪槽位命中区域与渲染坐标一致;越界返回 null 🟢

结论: REQUEST_CHANGES(偏轻) — 功能实现完整且覆盖 #4792 全规格,回滚与并发设计扎实;但 🔴1 的「一端改一端未改」会让 JEI 转移在副手/盔甲场景静默失败,建议修掉后再合并;⚠️2/⚠️3 是稳健性与性能上的可改进项,⚠️4 请确认是否为有意改动并补描述。


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

代码审查摘要 — PR #4806(Added Storage Fluid Port 添加了仓储流体端口)

操作: synchronize(分支 fluidtank/1.21/1.6dev/1.21/1.6
范围: 48 个文件(36 Java / 14 新增 / 0 删除;3 资源 + 7 资源包资源)| 4806 行 diff | 11 个提交
基准核对: 目标分支 dev/1.21/1.6 @ 32da04af1(本地克隆的 origin/dev/1.21/1.6 引用被并发进程改到了旧提交,已改用分支 tip SHA 核对)

⚠️ 说明:本环境无法运行 gradlew compileJava(webhook 无人值守、无构建缓存),因此未做真实编译;所有 API 存在性/签名均通过拉取目标分支源码逐条交叉核对(见下表),未发现编译风险。


✅ 交叉核对通过的关键 API(对目标分支源码验证)

调用点 目标分支实况 结论
GuiRenderSupport.blitSprite(..., blitOffset, w, h, color) 8 参数重载存在 ✓ 浮层 z 传参可用
FormattingUtil.toAbbrNum(long) 目标分支即为 long
FluidNetworkScanner.isPipePart(BlockState) / FluidNetworkManager.addContainer·removeContainer·markDirty(Level) 均为 public,签名一致
IFluidHandlerHolder(仅 getFluidHandler())/ FluidHandlerWrapper.tryInteractWithBottle(player, hand, handler, level, pos) FluidTankBlockEntity 调用完全一致
FluidTankItemTooltip.singleFluidTooltipImage + getTankTag(读 BLOCK_ENTITY_DATA.Tank 与端口 saveToDropTank 路径对得上 ✓ tooltip/物品液面能取到数据
AbstractWidget 3 参 onClick / isValidClickButton 目标分支已有先例(SwitchableButtonFluidDisplayWidget ✓ 且 button == 0 || button == 1 && … 优先级正确
StoragePortBlockEntity.CONNECTIVITY_LIMIT(private static)在同类静态方法内使用 / findSoleCore 重构 重构后的 validateLink 与原语义等价(原逻辑:非恰好 1 核心则 working=false,coreMainPos=null
CategoryEntry(ICategory) 默认 mode = UNLIMITED 默认列出的 4 个分类(含新 Fluid)不会过滤掉物品 ✓(这一点若默认是 ALLOWLIST 会隐藏全部物品,已排除)
Inventory.INVENTORY_SIZE(=36) vs getContainerSize()(=41) Inventory#additems 范围一致 ✓ 修复方向正确
CapabilitiesEventListener(be, side) -> be.getFluidHandler() 列表注册 FLUID_TANK 等同列,BE 实现 IFluidHandlerHolder
ModBlocks.ITEM_SPLITTER 改用 ChuteBlockItem CHUTE/MAGNETIC_CHUTE 已用同类 item(onItemUseFirst 贴容器放置) ✓ 与溜槽族一致

🔴 关键(建议合并前处理)

  1. StorageServerStub.withdrawNeedsFromStorages 现场盛装的顺序与同 PR 的 transferMaterialExact 相反
    produceFilledContainer 被放在「从存储提取现成物品」的循环之前StorageServerStub.java ~L3230),且传入的是 required(完整缺口)。后果:存储里明明存着成品水桶时,JEI 转移仍会优先抽走端口里的流体 + 吃掉空桶,把已有成品桶留在仓储里。这与 transferMaterialExact 中的注释「现成物品取完仍不足时,才用空容器 + 端口流体现场盛装」直接矛盾,玩家侧表现为"流体莫名减少、空桶莫名消失"。建议先取现成物品,再只对 required - moved 的缺口做现场盛装(produceFilledContainer 本身支持只补缺口)。

  2. 流体伪槽位编号依赖 StorageFluidRegistry.collect() 的下标,而该顺序不稳定

    • livePorts 遍历的是 Map<UUID, Set<BlockPos>> 里的 HashSet
    • StorageFluidPortBlockEntity.validateLink() 每 20 tick 无条件 unregisterpositions.remove)+ registeradd),HashSet 成员的 remove+add 会改变迭代顺序。

    于是 FLUID_SLOT_BASE + index 与具体流体的对应关系在多个端口/多种流体时可能在同一存档内漂移;StorageScreen.appendFluidSlots() 依赖 this.order.contains(BASE + index),一旦服务端缓存的 order 与本次同步的 fluids 列表错位,折叠显示下流体会莫名消失(或被追加到错误下标)。
    好消息:点击已改为携带流体身份(fluidIdentityfind()isSameFluidSameComponents 匹配),所以不会抽错流体。建议彻底一点:让 collect() 输出稳定顺序(如按 pos.asLong() 排序),或伪槽位编号由「端口位置/流体 id」派生;同时 validateLink 仅在归属真正变化时才 unregister/register,避免每 20 tick 无谓抖动。

  3. 物品分拣器默认朝向翻转,且未在 PR 描述中说明
    ItemSplitterBlock.getStateForPlacementgetHorizontalDirection().getOpposite()getHorizontalDirection()。而 ItemSplitterBlockEntity.getFacing() 的注释是「当前朝向,即均分方向」——这意味着默认输出方向与旧版相反,属于会影响现有玩家操作习惯的玩法变更(同时 .item().item(ChuteBlockItem::new) 让它可以贴容器放置)。建议在描述中明确说明并确认是有意为之,或拆成独立 PR;若只是为配合 ChuteBlockItem,请注意朝向翻转与"贴容器放置"是两件独立的事。

  4. JEI 可用池每个输入槽只补 1 个可盛装桶
    StorageJeiSupport.addProducibleFluidContainers 对每个槽 addAvailable(variant.copyWithCount(1), …)break。单槽需求 ≥2 桶的配方(requiredCountsByUid 已算出每槽需求量)仍会被判缺料——服务端 transferMaterialExact 有能力补足,但 JEI 的 getRecipeTransferOperations 是按可用池算的,会出现"检查通过、转移只填 1 个"。建议按该槽需求量补足。


⚠️ 警告

  • pourIntoFluidPort 文档与实现不符:javadoc 写「存在能接收该流体的端口(同种流体或空端口)」,但 StorageFluidRegistry.findAcceptor 明确只认同种流体、空端口返回 null(这符合 [TODO] 仓储流体端口 #4792 的需求)。请改文档,否则后续维护者会照"空端口可接收"的说明去改逻辑。
  • 转移失败回滚会把"成品桶"当物品退回存储transferMaterialExactfromFluid 回滚分支退回的是 wanted(装满的桶),而此前消耗的是背包里的空桶 + 端口流体。总量守恒,但形态被改变(玩家背包的空桶变成仓储里的水桶);注释称"等量、不会凭空增减"容易被误读为"原样归还"。refillFluid() + giveBackContainers() 已具备原样归还的能力,可考虑改用,或在注释中明确这是有意行为。
  • 偏置变化 → 整个维度管网重建adjustHeightBias() 每次 heightBias != previousFluidNetworkManager.markDirty(level)。两个端口在同管网内互相搬运时会反复进出死区(每 10 tick 最多一次全量重扫)。50%~75% 死区已是有效的滞回,但建议对大仓储场景做一次压测;若出现"端口之间来回倒液"的持续重建,可考虑加更宽的滞回或对 markDirty 做节流。
  • TerminalJeiTransferSupport.collectMissingdeficit <= 0deficit == 0 是等价改写deficit = Math.max(0, required - have)),无任何行为变化。如果这行原本是想修某个问题,请确认问题真的解决了。
  • lang 键只有 en_us/en_udcategory.anvilcraft.fluidscreen.anvilcraft.storage.fluid_amount…fluid.bucket_not_enough/ bucket_missingsrc/main/resources/assets/anvilcraft/lang/zh_cn.json 中 0 命中,而该文件在仓库内是随提交更新的(最近一次由 update dice (#4809) 改动)。若 zh_cn 在本仓库维护,请补齐;否则请确认由 Weblate 流程接手。
  • 描述未提及的额外改动(建议在描述/变更日志中列出或拆分):
    • ModelSelectionBakery 模式匹配风格重构(Fixed fixedFixed(SelectionPart part));
    • PlayerSetting.addCustom(ItemStack)FilterCategory 导入删除;
    • 贴图 textures/font/small.png共享小字体图集)与 textures/block/pipe_glass_node.png 被修改——前者影响所有用小字体绘制的界面(物品数量、流体数量都用它),请说明是补 m/B/单位等字符还是别的调整,最好附对比截图;
    • craftBudget():Ctrl+Q 连续合成预算从固定 64/单次产物 改为「按产物堆叠上限折算」(镐子类不可堆叠物品一次只合成 1 个,16 堆叠按 16 折算),是手感/平衡改动,建议写进更新日志。

💡 建议

  • javax.annotation.Nullableorg.jetbrains.annotations.Nullable 在同一 PR 内混用(BE/Registry/ItemRenderer/TexturedButton 用 javax,StorageFluidPortBlock 用 jetbrains)。1.21 分支上两者分别为 143/299 处,建议新代码跟随多数派统一。(AGENTS.md 里的 jspecify 规则在该分支不适用——分支上 jspecify 为 0 处。)
  • 右键流体格且指针非空时,指针物品会被当作普通物品存入仓储(走 PICKUP → 服务端 carried 分支)。这是刻意的(代码注释已说明),但玩家预期多半是"没反应",建议在 tooltip 或文档里点明"右键=按物品处理"。
  • 门格海绵清空:useItemOn客户端也执行 clearFluid()(改本地 tank 后由服务端同步纠正),且没有音效/GameEvent。功能正确,若想与储罐手感一致可补 playSound + GameEvent.FLUID_TAKE
  • 流体条目浮层用 FlyoutMessage 复用「缺失工作台」那套淡入淡出,z 注释(150/200/300/400)写得很到位;graphics.flush() 的时机说明尤其有价值,建议长期保留。

🟢 看起来不错

  • 新 BE 与既有 StoragePortBlockEntity 的写法高度对齐:saveToDrop/getCloneItemStack(Ctrl 拾取)/playerWillDestroy(创造模式敲掉带流体掉落)/onLoad → addContainersetRemoved → removeContainer 对称
  • 流体交互改为「按下标定位 → 按流体身份定位」,并让 InteractionResult 回传最新 carried,修掉了并发/列表变化下抽错流体与指针数量回传过期两个隐患 ✓
  • consumeCraftingInput 重写后与原版 ResultSlot.onTake 语义一致(剩余物:空槽放回 / 同种合并 / 否则交还玩家),修掉了原实现「剩余物直接覆盖原料导致原料凭空消失」的问题;保留"净变化为 0 ⇒ 未消耗"判定,防住了催化剂配方的无限产出 ✓
  • returnItems 三级去向(主去处 → 次去处 → 掉落世界)为回滚路径补上了兜底,避免中途吞物品 ✓
  • 序列化三处对称(saveAdditional / loadAdditional / getUpdateTag + getUpdatePacket),客户端渲染与 UI 读取数据链完整 ✓
  • 空端口不写入 BE 数据(保证与未放置物品可堆叠)、getDrops/创造模式掉落复用 saveToDrop,细节考虑周到 ✓

📋 声称验证表

PR 声称 状态 对应实现
新增仓储流体端口 StorageFluidPortBlock(+BlockItem/BlockEntity/Renderer/ItemRenderer)、ModBlocksModBlockEntitiesShapedRecipeLoader、blockstate/model/loot/recipe/advancement/tag/创造栏/分类数据 全套生成资源
resolved #4792 findAcceptor 只认同种流体端口 + pourIntoFluidPort⚠️ 但 javadoc 写成「或空端口」,与实现不符
增加了之前缺失的部分快捷键 TexturedButton 右键回调(存入按钮左/右键区分倾倒)、Shift+双击批量移入(lastQuickMoved + findInventorySlotWith)、craftBudget 预算折算;已确认新增的 isDoubleClick 调用与既有调用不冲突(Shift 分支总会 return,不会二次消费双击状态)
修改了 JEI 转移前的清空物品去向 clearCraftingreturnItems(..., storageFirst = true):存储优先 → 背包 → 掉落
修复会把物品放入盔甲栏的问题 5 处 getContainerSize()Inventory.INVENTORY_SIZEplaceCraftingResult*transferMaterialExacthasEnoughMaterialtransferFromInventorycountInInventorygiveBackToInventoryconsumeOne
(描述未提)物品分拣器朝向翻转 + 改用 ChuteBlockItem ⚠️ ItemSplitterBlock.getStateForPlacement / ModBlocks.ITEM_SPLITTER — 功能性行为变更,需说明
(描述未提)ModelSelection 风格重构 / PlayerSetting.addCustom(ItemStack) 删除 / small.png·pipe_glass_node.png 贴图 ⚠️ 与流体端口无关,建议拆分或说明;small.png 是共享字体图集,需确认

🧪 测试建议(未编译,纯静态分析;建议作者按此自查)

被测目标 建议场景 优先级
StorageFluidPortBlockEntity.adjustHeightBias() 空罐 → 灌满 → 反复取放,确认最终稳定在 50%~75% 且不反复横跳;断管网(无相邻管道)时偏置是否冻结、重接后 10 tick 内收敛 🔴
StorageFluidRegistry.collect()/livePorts() 3 个以上端口 + 2 种流体,反复拆装/改挂存储,校验 BASE+index 与流体身份是否始终一致(对应 🔴2) 🔴
StorageServerStub.withdrawNeedsFromStorages 存储同时存在"成品水桶 + 空桶 + 端口流体",验证是否优先用现成桶(对应 🔴1) 🔴
interact(FLUID_BUCKET / QUICK_MOVE_FROM_STORAGE) 指针空/拿空桶/拿满桶/拿石头 4 种情况 × 左/右键 × Shift;存量 999 mB 边界(提示"不足一桶" ) 🟡
produceFilledContainer() 流体/空桶各差 1 时回滚,确认物品与流体总量守恒(含跨维度端口与区块卸载端口) 🟡
consumeCraftingInput + autoRefillCrafting 桶返回类配方(水桶→空桶)、催化剂配方连续合成,确认不吞原料、不无限产出 🟡
StorageFluidPortBlockEntity 序列化 拆除带流体端口 → 重放 → 液面/tooltip 一致;空端口拆除后与新建物品可堆叠 🟢

结论:REQUEST_CHANGES(但整体质量高,仅需处理 🔴 四项) — 新方块/UI/JEI 三条主线的结构、生命周期对称性和序列化完整性都很扎实,注释质量明显高于平均水准。需要处理的是:withdrawNeedsFromStorages 现场盛装顺序(行为不一致)、流体伪槽位下标不稳定的稳健性问题、物品分拣器默认朝向翻转的说明/确认、JEI 单槽多桶的可用池数量;其余为文档与一致性建议。

关于 PR 标题: 当前标题格式合规(英文 + 中文),但它只体现了主要功能,未涵盖本 PR 同时包含的合成/JEI/背包修复与分拣器朝向变更。因本环境 gh 未认证(The token in …/hosts.yml is invalid),按约定未自动修改,建议改为:
Add Storage Fluid Port and storage UI fluid slots 添加仓储流体端口与界面流体槽位


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched SKILL.md in skill 'pr-diff-scope-reconciliation' (1 replacement).

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched SKILL.md in skill 'pr-diff-scope-reconciliation' (1 replacement). · Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement).

- 在英文和逆转文本中新增储液端口及其按住Shift的详细显示内容
- ItemTooltipManager中新增StorageFluidPort的普通与Shift提示文本
- 介绍储液端口的容量、连接规则、交互操作及破坏后保留流体机制
@Gugle2308

Copy link
Copy Markdown
Collaborator

⚠️ Dangerous command requires approval:

cd /tmp && grep -n "^diff --git" pr4806.diff > headers4806.txt && awk -F: '{print $1}' headers4806.txt > /dev/null; echo "headers: $(wc -l < headers4806.txt)"; cat > /tmp/extract.sh <<'EOF'
#!/bin/bas...

Reason: shell execution via heredoc

Reply /approve to execute, /approve session to approve this pattern for the session, /approve always to approve permanently, or /deny to cancel.

@Gugle2308

Copy link
Copy Markdown
Collaborator

审查完成。以下是审查意见(已按要求不做任何 gh 评论/发表操作;gh auth status 显示 token 已失效,故未自动改标题):


代码审查摘要 — PR #4806

操作: synchronize(PR 已从 draft 转为 ready for review,按完整流程审查)
head: 41cf46ab9b(上轮 38ad81ea5d → 本轮 +1 提交feat(tooltip): 添加储液端口的提示信息,3 文件 +14 行)
范围: 49 文件(37 Java / 14 新增 / 0 删除)/ 4852 行 diff;目标分支 dev/1.21/1.6


✅ 本轮新增提交(tooltip)逐条核对

tooltip 声称 实现依据 结论
Stores 128 B of a single fluid, never two at once CAPACITY_MB = 128 * 1000FluidTank 锁定首个流体
Connects to adjacent Storage Ports and Storage Fluid Ports, either can extend the chain StoragePortBlockEntity.findSoleCoreisPort() 同时识别 StoragePortBlock / StorageFluidPortBlock
Works even when linked to no storage(桶 / 管道直接读写) onPlayerUseFluidHandlerWrapper.tryInteractWithBottleFluidUtil.interactWithFluidHandler)与 getFluidHandler() 不依赖 storageIdCapabilities.FluidHandler.BLOCK 已为 STORAGE_FLUID_PORT 注册
The link only decides which storage lists its fluid validateLink() 只做 StorageFluidRegistry 登记
Right-click with a bucket or a bottle… / Menger Sponge to clear useItemOn:门格海绵 → clearFluid();其余 → onPlayerUse(先瓶后桶)
Keeps its fluid when broken getDropssaveToDrop;创造模式另走 playerWillDestroy
  • en_ud 逐行比对无误(6 行按倒序排列且每行字符翻转,与 en_us 一行行对应;NORMAL 单行同样正确)。
  • ✅ Java 文本块语法合法:内容行等缩进、结尾 """ 与末行同行 → stripIndent 后与生成文件(无缩进、\n 连接)完全一致。
  • 💡 可选:类 javadoc 强调「连通组件必须恰好接触一个核心」才登记(findSoleCore 在 0 个或 ≥2 个核心时都返回 null),而 tooltip 只写了 "The link only decides which storage lists its fluid"。玩家接成两条链各自碰一个核心时会以为端口坏了,可补半句。

⚠️ 仍未处理(上轮已报,本轮在 head 41cf46ab9b 逐条重新核实)

  1. terminalReorder 仍产出含伪槽位的 order → 终端浮窗流体条目不显示且反复空转
    StorageServerStub:2960 仍是 createOrder(view, options, search, categories)includeFluids = true);而 syncStorageServerStub:205)对 index >= view.size() 直接 continue,伪槽位永远进不了 TerminalRemoteOverlay.CONTENTS

    • syncFullContentsmissing 永不为空 → 每个刷新周期白发一轮 syncFullPage 页请求;
    • 终端里流体格渲染为空格子,terminalTake / terminalTakeToInventoryslot >= view.size() 静默 no-op(点了没反应)。
      修法:terminalReorder 改用 createItemOrder,或客户端 overlay 过滤 >= FLUID_SLOT_BASE
      (对比:服务端取物路径已正确改用 createItemOrder:3162 / :4063。)
  2. quickMoveSlotsToStorage 把「倾倒掉的桶」计入 undo
    StorageServerStub:433-435int inserted = moveInventoryStackToStorage(..., true) 的返回值语义是倾倒的桶数moveInventoryStackToStorage:3617 直接 return poured),却被 moved.merge(key, inserted) 当成「入库的成品桶」记进 undo → Ctrl+Z 会尝试还原存储里并不存在的桶(静默少还原)。moveSameToStorage / deposit 的空桶倾倒分支都显式计入 moved,此处不一致。

  3. withdrawNeedsFromStorages 现场盛装后无背包空间兜底(潜在物品丢失)
    StorageServerStub:3236-3240produceFilledContainer 已抽走端口流体并消耗空容器,随后 player.getInventory().add(resource.copyWithCount(produced)) 无空间检查——背包满时 add 返回 false、多余部分静默丢弃。同方法紧随的循环反而用了 getInventorySpace(注释写着「为防背包放不下导致 add 丢弃」),不一致本身即是信号。建议复用 giveEmptiedContaineraddItem(...) || Block.popResource(...) 兜底。

  4. JEI 可用池每槽只补 1 个可盛装桶
    StorageJeiSupport:447addAvailable(variant.copyWithCount(1), …) 后立即 break,而 requiredCountsByUid 已算出该槽可能需 ≥2 → getRecipeTransferOperations 只申请 1 个(服务端 transferMaterialExact 有能力补足但没被请求)。应按该槽需求量补足。

  5. 客户端 JEI 判定范围未随服务端收窄 → 静默 0 转移
    本 PR 把取用侧统一为 Inventory.INVENTORY_SIZE(修掉盔甲槽 bug),但两处「统计持有量」的判定侧仍用全身范围:

    • StorageJeiSupport:252 player.getInventory().getContainerSize()(含盔甲 36-39 + 副手 40)
    • TerminalJeiTransferSupport:384 for (Slot slot : container.slots)

    材料只在副手 / 盔甲时 JEI 判「可转移」,服务端 hasEnoughMaterial / transferMaterialExact / countInInventory / consumeOne 都取不到 → 玩家只看到「点了没反应」。不变量:判定范围 == 取用范围
    (反向提醒:restockHandgetContainerSize() 必须保留——副手槽 40 > INVENTORY_SIZE,改成收窄反而弄坏副手补货。)


💡 建议(非阻塞)

  1. 伪槽位编号依赖 HashSet 迭代顺序:StorageFluidRegistry:52 内层 new HashSet<>() + livePortsSet.copyOf;order(reorder)与 fluids(sync)来自两次独立 RPC,端口集合变动时可能把 A 流体的数量画到 B 的格子里(下次刷新自愈)。建议按「维度+坐标」或流体 id 排序使编号确定化。
  2. pourIntoFluidPort 外层 javadoc(StorageServerStub:5056)仍写「同种流体或空端口」,与 findAcceptor 实现(无同种流体端口即返回 null)相反——后人「按文档修复」正好会复活 [TODO] 仓储流体端口 #4792 的吞桶缺陷,建议删改。
  3. StorageFluidRegistry.positions(UUID):290)无任何调用者(javadoc 称「供测试与调试」,但本 PR 无测试)→ 收窄可见性或补测试。
  4. 端口 onContentsChanged 每次都调 StorageServerStub.onContentsChanged(storageId)(版本++ / orders.clear() / 邻居通知),而水位自调 + 管网分配会让端口持续进出液 → 打开该存储界面的玩家可能每 tick 走一次全量重排 + 分块 sync;对照物品端口 StoragePortBlockEntity.onContentsChanged 只做 setChanged + sendBlockUpdated。建议按数量变化阈值或 per-N-tick 节流。
  5. heightBias 不入 NBT 是有意设计(rememberedFluid 已声明),但字段注释没写明,建议补一句,否则后续会被当 bug 报。

🟢 本轮核实已修复(勿再报)

  • addFluidEntries 已补「普通文本放行」兜底分支(search.charAt(0) != '@' && != '#':4833-4836)→ 客户端 applySearchFilter 的流体名称/id 过滤重新可达;# 前缀有意不放行。
  • validateLink() 改挂:先 unregister(旧id, 维度, pos) → 双向 onContentsChanged(旧/新 id)setRemoved() 也通知归属存储 → order 缓存不再陈旧。
  • 取物路径改用 createItemOrdercreateOrder 拆 4 参含流体 / 5 参带 includeFluids 开关)。
  • hasFluidFor 已在 38ad81ea5d 改名为 lacksFluidFor,javadoc 与实现一致、全仓无旧名残留(上轮记录「仍在」有误)。
  • 客户端 6 处伪槽位守卫齐全(applySyncResult / applyPreservedSyncResults / resetServerSlots / hasContents / getStorageSlot / applySearchFilter / renderStorageContents);saveAdditional/loadAdditional/getUpdateTag/getUpdatePacket 四路径共用 TAG_TANK;能力注册、创造标签、pickaxe tag、10 个生成 JSON 完整。

📋 声称验证表

声称 状态 依据
resolved #4792(仓储流体端口) StorageFluidPortBlock/BlockEntity/Registry、注册链、生成资源、tooltip 全部到位
增加了之前缺失的部分快捷键 ⚠️ 需作者确认 diff 中未见键盘绑定(KeyMapping/GLFW)新增,可见的是修饰键交互:Shift+点流体格取桶入背包、Shift+双击/Alt 批量移入、左键倾倒 / 右键存桶、deposit 按钮右键。若指键盘热键请写明具体键位
修改了 jei 转移前的清空物品去向 clearCraftingreturnItems(..., storageFirst = true)(存储优先,放不下退背包),并有 Block.popResource 三级兜底
修复了会把物品放入盔甲栏的问题 ⚠️ 部分 137234d85a 把 6 处遍历改为 Inventory.INVENTORY_SIZE ✅;但客户端 JEI 判定侧未同步(见 ⚠️5)
(本轮新增)储液端口提示信息 NORMAL/SHIFT 文案与实现逐条一致;en_us/en_ud 已 datagen 同步

⚠️ 变更归属(与流体端口无关的改动 — 已跑 blob 三点对比)

本 PR 首个提交 878c5f6c23 同时改动了流体端口之外的三个文件(git show --stat 878c5f6c23 明确列出):

改动 PR base 32da04af17 / 目标分支 tip PR head 41cf46ab9b 判定
ItemSplitterBlock 默认朝向 getHorizontalDirection().getOpposite()getHorizontalDirection()(注释同步改写) 1a34af345d 29f80c97d5 本 PR 自身改动
ModBlocks.ITEM_SPLITTER.item().item(ChuteBlockItem::new) 同上文件组 同上 本 PR 自身改动
textures/font/small.png(401 → 958 B,共享位图字体,所有仓储 UI 数字都走它) 4f3f027dfc e590690d6a 本 PR 自身改动
  • 朝向一行在当前目标分支上由 add asset of spoofy redstone component (#4803) 引入,本 PR 改回了相反语义 → 这是对已合并行为的翻转,建议说明是否有意、或拆成独立 PRfont/small.png 影响全模组数字渲染,也请确认是否本 PR 有意改动。

🧪 测试建议

被测目标 推荐场景 优先级
StorageFluidRegistry.validateLink() A 存储 → 拆链 → 20 tick 内接 B:collect(A) 不含该端口、drain(A) 抽不到 B 的流体、两端 orders 缓存均失效 🔴
moveInventoryStackToStorage(pour=true) 调用点 倾倒 64 个水桶后 Ctrl+Z:undo 不应还原不存在的成品桶 🔴
withdrawNeedsFromStorages 背包只剩 0 格 + 仓储有流体+空桶:不得静默丢失成品桶(应掉落/退回) 🔴
addFluidEntries + StorageScreen.applySearchFilter 普通文本 / @ns / #tag / 空搜索下流体条目与服务端 order 的一致性 🟡
tooltip en_us / en_ud 行序与反转逐行一致、storage_fluid_port 两条键存在 🟢

验证方式说明:本轮为 webhook 无人值守会话,未运行 gradle 构建;tooltip 文案通过「生成 lang 文件 ↔ ItemTooltipManager 文本块 ↔ 实现源码」三方交叉核对完成,en_ud 逐行人工比对。

结论: REQUEST_CHANGES — 主功能(仓储流体端口)实现完整、与 #4792 规格逐条对齐,新增 tooltip 提交本身没有问题;但 ⚠️1(终端浮窗伪槽位)与 ⚠️2~5(undo 计数、补桶无空间兜底、JEI 可用池数量、客户端判定范围)四处仍未处理,其中 2/3/5 有物品丢失或静默失效风险,建议修复后再合并。

建议标题gh auth status 显示 token 无效,未自动执行 gh pr edit):Add Storage Fluid Port and fluid storage interactions 添加仓储流体端口与仓储流体交互


由 Hermes Agent 审查

@Gugle2308

Copy link
Copy Markdown
Collaborator

💾 Self-improvement review: Patched references/shell-only-diff-navigation.md in skill 'github-webhook-pr-review' (1 replacement). · Patched SKILL.md in skill 'anvilcraft-storage-system-review' (1 replacement). · Patched references/pr4806-storage-fluid-port-review.md in skill 'anvilcraft-storage-system-review' (1 replacement).

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.

[TODO] 仓储流体端口

2 participants