筛选界面与数据同步
基本信息
| 属性 | 值 |
|---|---|
| 容器 | com.encraft.dz.container.ContainerOreFinder |
| 客户端界面 | com.encraft.dz.gui.GuiOreFinder(@SideOnly(Side.CLIENT)) |
| 界面贴图 | assets/ifu/textures/gui/gui_ore_finder.png |
| 自定义库存 | com.encraft.dz.inventory.InventoryOreFinder |
| 玩家扩展属性 | com.encraft.dz.OreFinderPlayerData(IExtendedEntityProperties) |
| 界面标题 | gui.ifu_buildingKit.title(中文"寻矿魔杖") |
| GUI ID | 10 |
| 打开方式 | 潜行 + 右键(ItemOreFinderTool.onItemUse),服务端 openGui |
xSize / ySize 两个类都没有覆写,因此沿用 GuiContainer 的默认 176 × 166。
库存模型
| 项 | 值 | 来源 |
|---|---|---|
INV_SIZE |
2 | InventoryOreFinder.java:16 |
| 界面上实际暴露的槽位 | 1(SlotFilter 绑在 index 0) |
ContainerOreFinder.java:19 |
| index 1 | 没有对应的 Slot,只存在于 ItemStack[2] 数组里,界面上拿不到 |
— |
getInventoryStackLimit() |
1 | InventoryOreFinder.java:81-83 |
getInventoryName() |
gui.ifu_buildingKit.title |
InventoryOreFinder.java:71-73 |
hasCustomInventoryName() |
true |
InventoryOreFinder.java:76-78 |
isUseableByPlayer() |
恒 true(任何玩家都能开) |
InventoryOreFinder.java:95-97 |
isItemValidForSlot() |
!(stack.getItem() instanceof ItemOreFinderTool) && OreFinderSearch.isAcceptableFilter(stack) |
InventoryOreFinder.java:106-108 |
tagName |
"BuildingKit" |
InventoryOreFinder.java:14 |
markDirty() 会把 stackSize == 0 的槽位直接置 null,所以空筛选格读出来是 null 而不是一个空栈 —— ItemOreFinderTool 与 GuiOreFinder 都依赖这一点做判空。
槽位布局(共 37 个)
| 容器索引 | 数量 | 绑定 | 坐标 | 槽位类 |
|---|---|---|---|---|
| 0 | 1 | InventoryOreFinder 槽 0 |
(80, 26) | SlotFilter |
| 1 ~ 27 | 27 | 玩家主物品栏(j + i * 9 + 9) |
(8 + j×18, 84 + i×18) | 原版 Slot |
| 28 ~ 36 | 9 | 玩家快捷栏 | (8 + i×18, 142) | 其中恰好 1 个是 SlotLocked |
被锁定的是玩家当前手持的那一格:if (i == player.inventory.currentItem) 时改用 SlotLocked(ContainerOreFinder.java:29-33)。锁定目的就是防止玩家把正在生效的魔杖从界面里拿走。
SlotFilter / SlotLocked 子类枚举
⚠️ 事实澄清:
SlotFilter与SlotLocked没有任何子类。源码 grep 证据:$ grep -rn "extends SlotFilter\|extends SlotLocked" src/main/java (零命中)两个类各自在
ContainerOreFinder构造器里被实例化恰好一次(1 个SlotFilter+ 1 个SlotLocked),行为完全由它们自己覆写的方法决定。Slot的其余 35 个子类全部是原版Slot。
| 槽位类 | 实例数 | 覆写方法 | 行为 |
|---|---|---|---|
SlotFilter |
1 | isItemValid |
!(stack.getItem() instanceof ItemOreFinderTool) && OreFinderSearch.isAcceptableFilter(stack) —— 拒绝魔杖自己,拒绝完全无法解析的物品 |
SlotLocked |
1 | isItemValid |
恒 false |
SlotLocked |
同上 | canTakeStack |
恒 false |
SlotLocked |
同上 | putStack |
空方法,写入被丢弃 |
SlotLocked |
同上 | onPickupFromSlot |
空方法 |
SlotLocked |
同上 | getHasStack |
恒 false |
SlotLocked |
同上 | decrStackSize |
恒 null |
SlotLocked 通过 6 个覆写把该快捷栏格变成完全只读,任何取放路径都被堵死。
isAcceptableFilter(stack) = target.canSearch() || target.isBlocklisted(),含义是:被方块黑名单拒绝的物品仍然可以放进筛选格(放进去后界面顶部显示"黑名单方块!"警告),只有"完全解析不出目标"的物品才放不进去。
容器操作规则
transferStackInSlot(EntityPlayer player, int index):
| index | 行为 |
|---|---|
| ≠ 0 | 源是 SlotLocked → 直接 return null;否则从源只取 1 个(toPlace.stackSize = 1)放进筛选格,前提是筛选格当前为空且 isItemValid 通过;源里减 1,减到 0 则置 null |
| = 0 | 先遍历 1 ~ 36,把筛选物与"同物品(GTUtility.areStacksEqual)且未达堆叠上限"的槽位合并(每找到第一个就 break);若一条都没合并上,再找第一个空槽整体放入 |
slotClick(int slotId, int clickedButton, int mode, EntityPlayer player) 覆写了两条拦截:
- 该槽的
getStack() == player.getHeldItem()→ 直接return null,防止点击 / 拖动正在手持的魔杖 mode == 2(数字键换位)且clickedButton落在 0 ~ 8 → 目标快捷栏索引1 + (9 * 3) + clickedButton;若那是SlotLocked则return null
两条分支结尾都走 detectAndSendChanges()。
NBT 存档路径
筛选物存在玩家的扩展属性里,不存在魔杖的 NBT 里。
| 层级 | 字段名 | 来源 |
|---|---|---|
| 玩家 NBT | ExtendedPlayerProp(OreFinderPlayerData.EXT_PROP_NAME) |
saveNBTData → compound.setTag("ExtendedPlayerProp", properties) |
| 该标签下 | BuildingKit(InventoryOreFinder.tagName) |
writeToNBT → properties.setTag("BuildingKit", items) |
| 列表每项 | Slot(byte,槽位下标)+ ItemStack 标准 NBT |
item.setByte("Slot", (byte) i); getStackInSlot(i).writeToNBT(item); |
结构:
ExtendedPlayerProp
└── BuildingKit (NBTTagList)
├── { Slot: 0b, <物品 NBT> }
└── { Slot: 1b, <物品 NBT> }
不持久化的字段:OreFinderPlayerData 的 hasLastScan、lastScanX、lastScanY、lastScanZ、lastScanFilter 是纯内存扫描缓存 —— saveNBTData 只写 filterInventory,所以重登后 hasLastScan 归 false,会立即重新扫描一次。
copy(OreFinderPlayerData props) 也只复制 filterInventory(由 PlayerDataEventHandler.onClonePlayer 在玩家死亡 / 重生时调用),扫描缓存不复制。
一处遗留写法:readFromNBT 用 compound.getTagList(tagName, compound.getId()) 取列表,第二个参数传的是 compound 自身的 NBT 类型 id,而不是 NBTTagList 的 10。
网络消息
通道:NetworkRegistry.INSTANCE.newSimpleChannel("ifu")(PacketDispatcher),packetId 从 0 起递增,在 preInit 由 PacketDispatcher.registerPackets() 注册。
| 消息类 | 继承自 | packetId | 方向 | 载荷 | 何时发送 |
|---|---|---|---|---|---|
SyncPlayerDataMessage |
AbstractMessage.AbstractClientMessage |
0 | 服务端 → 客户端 | 一个 NBTTagCompound,内容即上文 ExtendedPlayerProp / BuildingKit 结构(OreFinderPlayerData.saveNBTData 的产物) |
玩家进世界时:PlayerDataEventHandler.onEntityJoinWorld 在 !event.entity.worldObj.isRemote 下用 PacketDispatcher.sendTo(msg, (EntityPlayerMP) event.entity) 只发给该玩家本人 |
process 在客户端执行 OreFinderPlayerData.get(player).loadNBTData(data),用收到的数据覆盖本地筛选格。
AbstractMessage 的三级结构(AbstractMessage.java):
| 抽象类 | isValidOnSide |
说明 |
|---|---|---|
AbstractMessage<T> |
恒 true(默认双向) |
基类;onMessage 先校验 side(不合法直接抛 RuntimeException),再调 process,玩家实体由 proxy 的 getPlayerEntity(ctx) 提供 |
AbstractClientMessage<T> |
side.isClient() |
只能服务端 → 客户端 |
AbstractServerMessage<T> |
side.isServer() |
只能客户端 → 服务端 |
PacketDispatcher.registerMessage 按子类决定注册哪一侧:AbstractClientMessage 子类注册 Side.CLIENT;AbstractServerMessage 子类注册 Side.SERVER;两者都不是则用同一个 packetId 在两侧各注册一次。
⚠️ 本 mod 没有任何
AbstractServerMessage的具体子类,也没有直接继承AbstractMessage的双向消息 —— 客户端 → 服务端的自定义消息数为 0(魔杖的扫描全部在服务端完成,客户端只负责播放音效与界面绘制)。
事件订阅
PlayerDataEventHandler(在 load() 里注册到 MinecraftForge.EVENT_BUS):
| 事件 | 处理 |
|---|---|
EntityEvent.EntityConstructing |
玩家实体构造时,若 OreFinderPlayerData.get(player) == null 则 register |
EntityJoinWorldEvent |
服务端:向该玩家发一次 SyncPlayerDataMessage |
PlayerEvent.Clone |
死亡 / 重生:把原玩家的 filterInventory 复制到新实体 |
界面文字与颜色
GuiOreFinder.drawGuiContainerForegroundLayer 画 4 类文字:
| 行 | lang 键 | 位置 | 颜色常量 | 默认色 |
|---|---|---|---|---|
| 标题 | gui.ifu_buildingKit.title |
居中于 x = 88,y = 12 | ColorUtils.title |
0x404040 |
黑名单警告(仅 isBlocklisted()) |
IFU.BlocklistedBlock(“黑名单方块!”) |
(27, ySize − 116 = 50) |
ColorUtils.blocklistWarning |
0x404040 |
| 名称行 | IFU.NameTip(“名称: %s”,筛选格为空时用 IFU.Empty) |
(10, ySize − 106 = 60) |
ColorUtils.displayText |
0x404040 |
| 搜索行(仅筛选格非空) | IFU.SearchTip(“搜索: %s”,参数是 MatchTarget.getLocalizedName()) |
(10, ySize − 96 = 70) |
canSearch() 为真用 searchAvailable,否则 searchUnavailable |
0x404040 / 0xAA0000 |
文字超宽时由私有方法 fit() 截断:trimStringToWidth(text, maxWidth - width("...")) + "...",其中 maxTextWidth = xSize - 10 - 8。
ColorUtils 用 GTNHLib 的 ColorResource.Factory("ifu") 声明 5 个颜色:
| 常量 | 默认值 | 用处 |
|---|---|---|
title |
0x404040 |
界面标题 |
displayText |
0x404040 |
名称行 |
blocklistWarning |
0x404040 |
黑名单警告 |
searchAvailable |
0x404040 |
搜索行(可搜索) |
searchUnavailable |
0xAA0000 |
搜索行(不可搜索,全文件唯一非深灰色) |
ColorResource 机制允许按名字覆盖这些默认值。这些颜色只用于筛选界面的文字,与矿石稀有度 / 矿层显示无关 —— ColorUtils 全文件仅这 5 个常量,唯一引用者是 GuiOreFinder。
背景层只是绑定贴图并 drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize),没有额外绘制逻辑。