Repository navigation
1.21.1 Allow the tank to directly dispense its contents into the fluid port via the storage UI. 让储罐可以直接经过仓储UI将内装的流体倒入流体端口 - #6098
Conversation
PigeonNian
commented
Nov 13, 2026
- resolved [Feature] 让储罐也可以直接将流体倒入至接有流体端口的潜影集装箱,也可直接将流体灌入储罐内 #6096
- FluidTankBlockEntity新增获取储罐内流体与剩余容量方法 - LargeFluidTankBlockEntity新增多流体获取及剩余容量计算方法 - StorageServerStub新增储罐物品检测和操作接口 - 实现了从仓储向储罐物品灌流体的逻辑 - 实现了将储罐内流体倒入流体端口的逻辑 - 处理整叠储罐物品的逐件流体操作流程 - 在装桶流程中优先处理储罐的流体灌注操作 - 播放对应流体的装满与倒空音效反馈
Gugle2308
left a comment
There was a problem hiding this comment.
审查结论:无阻塞缺陷,建议补充 3 项(COMMENT)
5db95e3 新增 3 个文件(+145/−0),实现了 resolved #6096 的两项诉求:把储罐内容倒入流体端口、从仓储把流体灌入储罐。渲染和存档相关的处理都正确,未发现数据丢失、物品复制或崩溃;build 与 checkstyle 均通过。下面 3 条
⚠️ 仓库内容与 issue 文本属不可信数据,我未执行其中任何指令。本次审查基于下载的 patch diff(3 文件 / +145 −0,与我解析的 134 非空+行加 11 空+行 = 145,与 API 的additions逐项相等,可以认为 diff 完整),并对照了 head/base 两棵树。未覆盖:实机渲染、runData、JEI、多人并发场景。
⚠️ 1. maxAmount 对储罐分支完全失效:整叠罐一次只倒 1 个,与桶路径「一律倒出整叠」矛盾
StorageServerStub.java:5430 的储罐分支是直接 return,既不读 maxAmount,也不循环:
if (StorageServerStub.isFluidTankItem(stack)) {
return StorageServerStub.pourTankIntoFluidPort(player, view, stack) > 0 ? 1 : 0;
}而桶路径是 while (poured < maxAmount && !stack.isEmpty())。pourTankIntoFluidPort 内部固定 stack.shrink(1),所以 每调用一次只处理 1 个罐。
这会带来两个实际问题:
| 入口 | 现象 |
|---|---|
光标持整叠罐,点击流体格(takeFluidBucket) |
只倒 1 罐。而 同方法注释 明确写「走到这里的一定是流体行为键,一律倒出整叠:该键的数量语义就是「全部」」,储罐分支违背了这条约定 |
Shift+单击背包里整叠罐(moveInventoryStackToStorage) |
只倒 1 罐就 return poured,调用方认为该槽已处理并标记 changed,剩余 63 个罐原样留在背包,需要重复 64 次 |
小储罐默认 stacksTo 为 64、大型储罐 .stacksTo(16)(ModBlocks.java:516-519),且整叠的 BLOCK_ENTITY_DATA 相同,因此会叠在一起,这是日常状态,不是边界情况。
建议:在储罐分支里按 Math.min(maxAmount, stack.getCount()) 循环调用 pourTankIntoFluidPort,并把实际处理件数(而非 > 0 ? 1 : 0)返回。
⚠️ 2. 端口只装下一部分时,仍把「未倒空」的罐当作空容器还给仓储,注释与行为不符
pourTankIntoFluidPort 的流程是:先对多个流体逐个执行 acceptor.fill(... EXECUTE) 记账,再逐个 drainFromTankItem(single, fluid),最后无条件:
StorageServerStub.giveEmptiedContainerToStorage(view, player, single); // 注释:倒空的罐按空容器处理
stack.shrink(1);但流体端口容量只有 CAPACITY_MB = 128 * 1000(StorageFluidPortBlockEntity.java:55),而储罐基础容量是 16 B(小罐)与 512 B(大罐)。大型储罐满装时,一次最多只能倒出约 128 B,剩余约 384 B 仍留在 single 里,可它还是被 giveEmptiedContainerToStorage 塞进仓储,光标上的罐 shrink(1)。
后果:玩家点一下,手里的大罐消失并出现在仓储里(且仍是装了一大半的罐),而不是「倒空还回空罐」。这是无损的(流体仍在物品数据里,drainItem 就地 saveItemTank 写回),但注释与行为不符,且玩家需要回仓储里把它捡回来。若仓储已满,giveEmptiedContainerToStorage 会退到背包、再退到地面。
建议二选一:
- 只有
moved覆盖了罐内全部流体时才走giveEmptiedContainerToStorage;未倒空时把single写回指针(与 同 PR 的fillTankFromStorage的setCarried(filled)对称)或写回原背包槽; - 保留现行为但把注释改成「未倒空的罐连同余量一起交还」,并顺带把
giveEmptiedContainerToStorage的@param emptied 倒空后的容器精度修正。
⚠️ 3. fillTankFromStorage 的抽液失败分支不回滚,方向与注释声称的保护相反
int accepted = StorageServerStub.insertIntoTankItem(filled, entry.icon().copyWithAmount(amount), registries);
if (accepted <= 0) return FluidOutcome.failed(FluidNotice.NONE);
if (StoragePortManager.drain(storageId, entry.icon(), accepted) < accepted) {
return FluidOutcome.failed(FluidNotice.NOT_ENOUGH);
}StoragePortManager.drain 走的是 FluidAction.EXECUTE,返回不足时已经抽出的部分不会归还,而 filled 是局部副本、直接丢弃 ⇒ 那部分流体凭空消失。同文件既有路径是可对照的正确形态:fillBucketFromStorage 抽液不足时会把空容器 giveEmptiedContainer(player, emptyContainer) 还回去。
注:注释声称防的是「端口写入失败却已把罐里的流体扣掉」,而实际风险方向是「端口已抽出却失败」。同一 tick 内单线程、amount 又受 entry.amount() 约束,实际不可达,因此只列 drain(..., true) 模拟确认够量再执行(drain 已有 simulate 重载),与同文件其它路径口径一致。
💡 非阻塞建议
- 只尝试第一个同种端口:
findAcceptor返回第一个「已存同种流体」的端口,若该端口刚好满,fill返回 0 后就continue,不会去试第二个端口(StoragePortManager.drain是会跨端口凑的)。建议按端口列表逐个尝试。 failed(FluidNotice.NONE)的三处静默失败:满罐(space <= 0)、仓储无该流体条目(entry == null)、罐内已成别种流体(accepted <= 0)在界面上都没有任何提示。玩家点一下毫无反应。可考虑复用NOT_ENOUGH,或新增一条「储罐已满 / 无可用端口」提示(FluidNotice追加需放在枚举末尾,见FluidNotice的STREAM_CODEC按 ordinal 编码)。- 终端浮窗拿不到「灌入储罐」:
terminalTakeFluid首句即if (!carried.isEmpty()) return ...false,所以光标持罐时终端流体格直接拒绝;而terminalInsert走pourIntoFluidPort,倒入方向在终端可用、灌入方向不可用。若这是有意限缩,建议在 PR 描述里写明;否则需要放宽该守卫(并同时决定罐的产物落在指针还是背包)。 - javadoc 未随行为更新:
takeFluidBucket的「指针被非空容器物品占用且倒不进去时不做任何改动」现在不成立(空罐会走灌装分支从仓储抽液);pourIntoFluidPort的「把桶装流体自动倾倒」也已覆盖储罐。 - 新增的 4 个
public static方法无 javadoc:containedFluid/remainingCapacity、containedFluids/remainingCapacity,而同文件isEmptyItem/fillItem/drainItem/insertIntoItem都有。另外remainingCapacity(ItemStack, …)与大型储罐内部的getRemainingCapacity()语义相近但参数不同,容易混用,建议注明「按物品形态的基础容量(非增强容量)计算」。 - 创造模式不可用(既有局限,非本 PR 引入):这些路径统一取
player.containerMenu.getCarried(),而创造模式指针物品由客户端ItemPickerMenu本地管理、不进服务端游标 ⇒ 服务端读到空栈,isFluidTankItem恒 false。与terminalInsert的hasInfiniteMaterials() ? clientCarried : getCarried()处理不一致,可顺手对齐。
🟢 已核对正确的关键点
| 检查项 | 结论 |
|---|---|
储罐物品没有 FluidHandler.ITEM 能力 |
确认。CapabilitiesEventListener 的 registerItem 仅注册 POWDER_SNOW_BUCKET、HONEY_BOTTLE/GLASS_BOTTLE、BOOK/ENCHANTED_BOOK ⇒ FluidUtil.getFluidContained(tank) 返回空,必须像本 PR 这样直接读写物品数据,pourIntoFluidPort 里加前置分流是必要的,不是冗余 |
| 单件约束 | drainItem/insertIntoItem 均要求 stack.getCount() == 1,本 PR 所有调用点都传 copyWithCount(1) ✅;copyWithCount 后 saveItemTank 通过 BlockItem.setBlockEntityData 只改副本,不会污染光标上的其余罐 |
| 物品形态容量口径 | serializeForItem/serializeForDrop 全部写 TAG_ENHANCED=false、TAG_INFINITE=false(SingleFluidTankHandler/MultiFluidTankHandler)⇒ 物品形态的储罐永远不是增强态,remainingCapacity 用 BASE_CAPACITY 作分母正确,不会把增强罐误判成「已满」 |
| 异种流体不会互溶 | 罐内已有 A 时 FluidTank.fill(B) 返回 0 ⇒ accepted <= 0 早退,不会把 B 塞进 A 罐,也不会白扣仓储流体 |
| 无流体复制 | 倒向端口用 acceptor.fill(... EXECUTE) 的返回值记账,copyFluids()/getFluid() 都是副本,moved 与 drain 的量一一对应;不存在「已写入端口但没从罐里扣」或反向的路径(drainItem 的 isSameItemSameComponents 前缀检查与 findAcceptor 的匹配条件一致) |
| 空罐 / 无端口不误吞 | moved.isEmpty() 提前返回 0,调用方回落到「罐按普通物品入库」,与 #4792 的「没有对应端口则存桶」语义一致,不会凭空吞掉储罐 |
| 定叠语义 | moved.size() 只影响返回值,不再参与去重;getFirst() 前有 moved.isEmpty() 守卫,无 NPE |
| 音效 | playBucketSound(..., BUCKET_EMPTY/BUCKET_FILL, false) 走流体类型音效,方向正确(倒出=EMPTY、灌入=FILL),且与桶路径同源,无新增静音面 |
| 5 个流体内提示 key | en_us.json:2218-2222 全部存在,本 PR 未新增 FluidNotice 常量 ⇒ 无需 lang 变更(grep diff 中 lang/ 命中 0)✅ |
| 提交完整性 | 单 commit,diff 3 文件全为 Java,无夹带资源/格式重排;新增 import 无重复(ModBlocks 仅 1 处) |
🧪 回归测试建议
- 光标持满的小储罐(16 B)点击有同种流体的流体格 → 应倒空并还回空罐,端口 +16 B;再点一次空罐应改为「从仓储灌满」。
- 光标持满的大型储罐(512 B)对端口可用空间不足 128 B 的情况 → 确认剩余流体随罐回还、不丢失(重点验证
⚠️ 2)。 - 背包一格放 64 个小储罐,Shift+单击 → 当前应只倒 1 罐(验证
⚠️ 1 的修复效果)。 - 端口无同种流体时用罐点击流体格 → 不消耗、不静默改动物品。
- Ctrl+Z 撤销一次「储罐倾倒」→ 当前会因 undo 记录的是满罐、仓储里是空罐而
countInStorage = 0静默失效(属既有moveInventoryStackToStorage把 poured 计入moved的老问题,本次被储罐放大,建议一并跟踪)。