合成网格操作(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 版本(服务端):

  1. 遍历 [start, start+size) 区间,对每个 container.inventorySlots.get(i) 取 Slot.getSlotIndex() 作为 craftMatrix 下标
  2. 只收集 itemStack.getMaxStackSize() > 1 的堆叠(不可堆叠物被完全跳过)
  3. 用 gtnhlib 的 ItemStackMap<>(true)(true = NBT 敏感)按物品分组
  4. 每组算 totalCount = Σ stackSize,然后
    • countPerStack = totalCount / 槽位数,先给每槽平铺这个基准值
    • restCount = totalCount % 槽位数,从第 0 槽起循环 +1, 跳过已达 getMaxStackSize() 的槽
  5. 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)(id 5)
  • 无服务端 → 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 把背包物品投进网格」。

相关条目