合成网格操作(Grid Tweaks)
基本信息
| 属性 | 值 |
|---|---|
| 默认实现 | net.blay09.mods.craftingtweaks.DefaultProviderV2Impl(经 SimpleTweakProviderImpl 委托) |
| 旧版实现 | DefaultProviderImpl(DefaultProvider 接口,仅经 CraftingTweaksAPI.createDefaultProvider() 暴露给第三方) |
| 客户端实现 | net.blay09.mods.craftingtweaks.client.ClientProvider(全 windowClick 模拟点击) |
| 操作种类 | 5 种:旋转 / 均分 / 平铺 / 清空 / 强制清空 |
| 附带操作 | 正键点击投料(forward-click transfer),不在按钮与热键体系内 |
这是本 mod 实际提供的「合成机制」:它不新增任何方块、物品或配方,
而是对已登记容器的合成网格做矩阵级搬运。5 种操作全部对
IInventory(craftMatrix)或容器 slot 列表原地操作。
三套实现的分工
| 实现 | 运行侧 | 手段 | 前置条件 |
|---|---|---|---|
DefaultProviderV2Impl |
服务端 | 直接读写 IInventory |
无(服务端可信) |
ClientProvider |
客户端 | 模拟 PlayerControllerMP.windowClick |
!provider.requiresServerSide() |
DefaultProviderImpl |
双侧通用 | 直接读写 IInventory |
无 |
选边规则(ClientProxy.onClientTick / onActionPerformed):
CraftingTweaks.isServerSideInstalled == true → 全部走封包(服务端 DefaultProviderV2Impl);
false → 客户端 ClientProvider 本地模拟。
⚠️ 由此产生一个明确的能力差异:ClientProvider 的
canBalance / canClear / canRotate / canTransfer 全部以
!provider.requiresServerSide() 为前提,而 requiresServerSide() 在
SimpleTweakProviderImpl 里就是 phantomItems 字段。
所以在纯客户端(服务端未装本 mod)下,phantomItems 容器上的
均分/清空/旋转/投料会静默失效。
旋转映射表
DefaultProviderImpl、DefaultProviderV2Impl、ClientProvider 三处各有一份
完全相同的匿名 RotationHandler;另有 2 份相同的小号 handler
(SimpleTweakProviderImpl.smallRotationHandler 与
TerraFirmaCraftTweakProvider.smallRotationHandler)。
9 格 handler(3×3)
ignoreSlotId(4) == true —— 正中心格永不参与旋转。
顺时针(counterClockwise = false):
| 源槽 | 0 | 1 | 2 | 3 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| →目标槽 | 1 | 2 | 5 | 0 | 8 | 3 | 7 | 6 |
逆时针(counterClockwise = true):
| 源槽 | 0 | 1 | 2 | 3 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| →目标槽 | 3 | 0 | 1 | 6 | 2 | 7 | 8 | 5 |
未列出的槽(含 4)返回 0。实现方式是先整体克隆网格到
InventoryBasic,再逐槽写回,避免覆盖丢失。
4 格 handler(2×2,用于玩家背包 2×2 与 TFC)
ignoreSlotId 恒为 false(无中心格)。
| 方向 | 0→ | 1→ | 2→ | 3→ |
|---|---|---|---|---|
| 顺时针 | 1 | 3 | 0 | 2 |
| 逆时针 | 2 | 0 | 3 | 1 |
⚠️ SimpleTweakProviderImpl.rotateGrid 按
getCraftingGridSize(...) == 4 自动选小号 handler。
但 ClientProvider.canRotate 要求 size 必须 == 9,
所以纯客户端下 2×2 网格的旋转不可用(走服务端才生效)。
客户端旋转的两级实现
| 实现 | 条件 | 手段 |
|---|---|---|
rotateGridWithBuffer |
玩家背包有 ≥ 2 个空槽 | 双槽交替做缓冲,顺序最少的搬运序列 |
| 循环 pick-up/put-down | 上述失败 | 从 startSlot 起沿映射表走一圈 |
两者都先 dropOffMouseStack 把鼠标上的东西放回背包。
均分(balanceGrid)
DefaultProviderV2Impl 版本(服务端):
- 遍历
[start, start+size)区间,对每个container.inventorySlots.get(i)取Slot.getSlotIndex()作为 craftMatrix 下标 - 只收集
itemStack.getMaxStackSize() > 1的堆叠(不可堆叠物被完全跳过) - 用 gtnhlib 的
ItemStackMap<>(true)(true= NBT 敏感)按物品分组 - 每组算
totalCount = Σ stackSize,然后countPerStack = totalCount / 槽位数,先给每槽平铺这个基准值restCount = totalCount % 槽位数,从第 0 槽起循环+1, 跳过已达getMaxStackSize()的槽
container.detectAndSendChanges()
效果是把 3 摞 10 个的物品变成 10/10/10;把 3 摞共 10 个变成 4/3/3。
ClientProvider.balanceGrid 是另一套算法,纯 windowClick:
按物品分组后求 average = floor(Σ / 槽位数),对每个 stackSize > average
的槽执行 pick-up,再向右低于 average 的槽逐个右键补 1,最后放回。
平铺(spreadGrid)
DefaultProviderV2Impl.spreadGrid,仅在按住 Shift 时触发:
while (true):
找出 [start, start+size) 内 stackSize > 1 的最大堆叠 biggestSlotStack
若不存在 → return
对每个空槽:若 biggestSlotStack.stackSize > 1 → splitStack(1) 铺一个
否则 → 置 emptyBiggestSlot = true
若 emptyBiggestSlot → 继续循环
否则 break
balanceGrid(...) // 平铺结束必定再跑一次均分
即「先摊到每格最多 1 个的极限形态,再均分」。
ClientProvider.spreadGrid 是等价的 windowClick 版本。
清空(clearGrid)
DefaultProviderV2Impl.clearGrid(provider, id, player, container, phantomItems, forced):
对 [start, start+size) 每个槽:
| phantomItems | forced | 行为 |
|---|---|---|
true |
任意 | 直接 setInventorySlotContents(slotIndex, null) |
false |
false |
addItemStackToInventory(itemStack);失败则 continue(物品留在槽里) |
false |
true |
addItemStackToInventory 失败则 dropPlayerItemWithRandomChoice(itemStack, false) 丢出,随后无条件 setInventorySlotContents(slotIndex, null) |
最后统一 detectAndSendChanges()。
ClientProvider.clearGrid 走 windowClick(i, 0, 1) 整摞取出 +
container.transferStackInSlot(player, i),forced 时再补一次
windowClick(i, 0, 4)(丢弃)。
正键点击投料(forward-click transfer)
不在按钮/热键体系内,走 GuiClickEvent:
- Mixin 在
GuiScreen.handleMouseInput注入 →CraftingTweaks.onGuiClick发事件 ClientProxy.onGuiClick(EventPriority.LOWEST)检测keyBindForward(W 键) 被按下且鼠标悬停槽有物品- 按住 Shift → 把容器内所有
isItemEqual+ NBT 相同的槽一起收进投料列表 - 有服务端 → 每个槽发一个
MessageTransferStack(0, slotNumber)(id5) - 无服务端 →
clientProvider.transferIntoGrid(...)
服务端 HandlerTransferStack 的安全校验:
slotNumber必须在container.inventorySlots范围内canTransferFrom通过(SimpleTweakProviderImpl里即slot.inventory == entityPlayer.inventory)slot instanceof SlotCrafting一律拒绝(源码注释:SlotCrafting is always blacklisted)slot.canTakeStack(player)必须为真transferIntoGrid前后比对slotStack.stackSize,无变化则提前 return
因为只认玩家背包(slot.inventory == entityPlayer.inventory),
这个操作实际用途是「在合成台界面里按 W 把背包物品投进网格」。