# -*- coding: utf-8 -*- """ ================================================================================ 客户端右键拦截系统(ClientFeatureDispatcher) ================================================================================ 【作用】 在客户端提前拦截"手持物品右键功能方块"的默认行为(吃/放置前摇), 消除服务端取消带来的"闪一下"视觉问题。与服务端 FeatureDispatcher 配合: 客户端先 cancel/ret, 再由服务端精确处理转换。 【监听事件】(全模组客户端唯一拦截点) 1. ClientItemUseOnEvent 手持白名单物品右键方块表面 -> ret=True 2. ClientItemTryUseEvent 手持直接吃物品(饭团/水煮蛋/豆腐) -> cancel=True 【注意】不拦 ClientBlockUseEvent(右键功能方块本体): 那会误拦"对炉灶放置 蒸笼/对空锅放置盘子"等合理放置。右键方块本体的转换由服务端 OnBlockUse 精确路由处理, 不在客户端提前 cancel。 【白名单来源】 共享数据 InterceptData.py (客户端/服务端同一份, 见其注释) ================================================================================ """ import time import mod.client.extraClientApi as clientApi ClientSystem = clientApi.GetClientSystemCls() clientComp = clientApi.GetEngineCompFactory() from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxClientSystem from .InterceptData import (ITEM_USE_ON_CANCEL, ITEM_USE_ON_CANCEL_BY_ITEM, SUSHI_BLOCKS, SUSHI_CANCEL, COATING_ITEMS, TAKEOUT_CANCEL, GAI_FAN_CANCEL, BLOCKUSE_CANCEL, BLOCKUSE_CANCEL_ITEMS, BLOCKUSE_CANCEL_ANY_BLOCKS, PLACE_ITEMS, EAT_ITEMS) # 自定义事件: 客户端拦(碗/盘,汤锅/炒锅/油锅成品)放置预览后, 显式通知服务端执行盛出 # 服务端在 FeatureDispatcher 用同一事件名监听(见 service/FeatureDispatcher.py) TAKEOUT_EVENT = "TakeoutRequest" # 自定义事件: 客户端拦(成品菜/米饭, 米饭/成品菜/蛋炒饭) 盖饭转换的放置预览后, # 显式通知服务端执行盖饭转换(同盛出机制)。若不显式通知, 客户端 ret=True 会吞掉 # 服务端 ServerItemUseOnEvent, 导致盖饭"点无反应"。 GAI_FAN_EVENT = "GaifanRequest" # 自定义事件: 客户端拦【其余全部 itemUseOn 转换】的放置预览后, 显式通知服务端执行(同盖饭)。 # 覆盖: 面食(生面饼烙馍/面粉加鸡蛋变面糊/生食蒸笼)、汤锅(锅态汤->汤面)、原版装盘(南瓜派 # 等->盘中餐)、相邻放置(放死鱼/生鸡块/切菜)、厨具蘸油(锅铲/带油锅铲)、寿司、裹粉、 # 种水稻. 若不显式通知, 客户端 ret=True 会吞掉 ServerItemUseOnEvent, 这些转换"点无反应"。 ITEM_USE_ON_REQUEST = "ItemUseOnRequest" # 自定义事件: 客户端拦(手持可放置/可投掷物品, 功能方块)放置预览后, 显式通知服务端执行 # BlockUse 转换(根因B). 例如手持 生鸡肉/鳕鱼/鲑鱼 右键 支杆 -> 上杆; 死鱼右键挂肉钩 -> # 挂放; 鸡蛋右键鸡窝 -> 放蛋; 胡萝卜右键饲料槽 -> 加料; 盘子右键丸子机/石磨 -> 盛取; # 木条右键炉灶 -> 点火. 客户端 ret 掉放置/投掷预览后 NotifyToServer, 服务端按被点方块 # blockOwnerAll 路由到 handleBlockUse(与 TakeoutRequest 同机制). BLOCK_USE_REQUEST = "BlockUseRequest" class ClientFeatureDispatcher(BlueFoxClientSystem): """客户端右键拦截: 白名单提前 cancel/ret, 消除闪烁""" def __init__(self, namespace, systemName): BlueFoxClientSystem.__init__(self, namespace, systemName) print "===== ClientFeatureDispatcher init =====" # 盛出/盖饭/出锅 后的客户端放置冷却: key=playerId@(x,y,z), value=时间戳. # 转换完成后一段时间内, 该位置再被当作"放置"会直接 ret 吞掉, 消除"转换后 # 由于引擎重置右键导致多放置一次"的闪一下。时长 1.5 秒(用户要求明显可见)。 self._afterCd = {} # BlockUse 转换(手持食材/鸡蛋右键功能方块)的独立短冷却: 回调方块后的 0.2s 内 # 该坐标再被当作右键会直接 ret 吞掉, 防止"放蛋后连点被误当投掷扔出去"。 # 时长 0.2 秒(用户指定"稍微停一下0.1~0.2秒"); 翻炒(0.3s 间隔)不受影响。 self._blockCd = {} # 玩家级"刚做过 BlockUse 转换(放蛋/放料)"短标记: key=playerId, value=时间戳. # 用于 OnItemTryUse(投掷/使用轨道)在 0.2s 内取消该玩家的投掷动画(如鸡蛋右键鸡窝 # 放蛋后引擎重置右键立即触发投掷 -> 扔蛋)。cd 到即放行, 对地扔蛋正常。 self._blockCdPlayer = {} # 玩家级"刚对垃圾桶等'任意物品'方块做过拦截"短标记: key=playerId, value=时间戳. # 垃圾桶规则是 item="*any", 无法用 item 精确判断, 故用玩家级窗口在 0.2s 内取消 # 同一右键伴随的那次"吃/用"(如手持葡萄右键垃圾桶, 取消吃葡萄)。 self._anyBlockPlayer = {} self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ClientItemUseOnEvent", self, self.OnItemUseOn) self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ClientItemTryUseEvent", self, self.OnItemTryUse) def Destroy(self): print "===== ClientFeatureDispatcher Destroy =====" self.UnListenForEvent( clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ClientItemUseOnEvent", self, self.OnItemUseOn ) self.UnListenForEvent( clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ClientItemTryUseEvent", self, self.OnItemTryUse ) # ============================================================================ # 入口1: 手持物品右键方块表面 -> 白名单组合才 ret (取消默认放置/吃) # ============================================================================ def OnItemUseOn(self, args): blockName = args.get("blockName") try: itemDict = args.get("itemDict") or {} itemName = itemDict.get("newItemName") or itemDict.get("itemName") except Exception: itemName = None if not itemName: return # 转换后冷却: 同一玩家同一坐标, 若刚做过盛出/盖饭/出锅, 短时间内再被引擎 # 当作"放置" -> 直接吞掉(不触发服务端, 也不渲染放置预览). 消除"转换后 # 由于引擎重置右键导致的多放置闪烁"。 pId = args.get("entityId") or args.get("playerId") cdKey = "%s@%s_%s_%s" % (pId, args.get("x"), args.get("y"), args.get("z")) _t = self._afterCd.get(cdKey) if _t is not None and (time.time() - _t) < 1.5: args["ret"] = True return # BlockUse 转换短冷却: 放蛋/放料后 0.2s 内同一坐标再被当作右键(引擎重置导致的 # 二次使用/投掷)直接吞掉, 防止"连点鸡窝被误当扔蛋"。时长 0.2s(用户指定, 不卡翻炒)。 _bcd = self._blockCd.get(cdKey) if _bcd is not None and (time.time() - _bcd) < 0.2: args["ret"] = True return # 【根因B】手持可放置/可投掷物品 右键功能方块(支杆/挂肉钩/鸡窝/饲料槽/丸子机/石磨/ # 炉灶/寿司/炒锅放料 等) -> 此组合是 BlockUse 转换(服务端 handleBlockUse), 不能走下面 # ITEM_USE_ON_CANCEL_BY_ITEM 的"相邻放置"(那会绕开 owners 检查把死鱼/死鸡放到地面)。 # 此处 ret 掉放置/投掷预览 + 显式 BlockUseRequest, 由服务端按被点方块 handleBlockUse。 # 【注意】这里设 _blockCd(0.2s 短冷却): 放蛋/放料转换后引擎重置右键会再触发一次 # "使用/投掷", 若这次也被当正常右键会扔蛋/放着。0.2s 内同坐标直接吞掉, 既消扔蛋又 # 不卡翻炒(翻炒 0.3s 间隔 > 0.2s)。去重同时靠服务端 handleBlockUse 自带 checkCooldown。 if blockName and (itemName, blockName) in BLOCKUSE_CANCEL: args["ret"] = True self._blockCd[cdKey] = time.time() self._blockCdPlayer[pId] = time.time() # 玩家级短标记, 取消随后 0.2s 内投掷动画 self.NotifyToServer(BLOCK_USE_REQUEST, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) return # 【垃圾桶等"任意物品"方块】该方块的规则是 item="*any", 不参与 (item, block) # 精确派生, 无法按 item 判断, 故按被点方块整块拦截: 手持任意非空物品右键垃圾桶 # (关/开/有垃圾三态)时, 原版会先执行 吃(葡萄等食物)/放置(胡萝卜等方块), # 服务端再取消已晚 -> "一边吃一边清空"/"直接放置"。此处 ret 掉默认行为消闪, # 再显式 BlockUseRequest 让服务端照常 清空主手/开盖; 同时打玩家级标记, 取消 # 同一右键伴随的那次"吃"(详见 OnItemTryUse)。 if blockName and blockName in BLOCKUSE_CANCEL_ANY_BLOCKS: args["ret"] = True self._blockCd[cdKey] = time.time() self._blockCdPlayer[pId] = time.time() self._anyBlockPlayer[pId] = time.time() self.NotifyToServer(BLOCK_USE_REQUEST, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) return # 纯手持物品取消 (相邻放置/手持吃物品右键表面): 只看 item, 服务端走 handleItemUseOn # (Yuanban 相邻放置/装盘、Natural 种水稻、Shiyongfood 直接吃)。这些组合一旦客户端 # ret=True 会吞掉 ServerItemUseOnEvent, 故必须显式通知服务端补齐(盖饭同款机制)。 # 【注意】此分支为"相邻格放置/装盘", 被点【表面坐标】不变, 不能设 _afterCd, # 否则会误吞同一表面 1.5s 内的连续放置. 去重交给服务端 handleItemUseOn 自带冷却。 # 同时打玩家级短标记(_blockCdPlayer): 同一右键配套的吃(ClientItemTryUseEvent) # 在 0.2s 窗口内被取消 —— 只取消"对着方块右键放置"伴随的那次吃, 空中正常吃原版食物 # (胡萝卜/土豆/干海带/生肉鱼)不受影响。 if itemName in ITEM_USE_ON_CANCEL_BY_ITEM: args["ret"] = True self._blockCdPlayer[pId] = time.time() self.NotifyToServer(ITEM_USE_ON_REQUEST, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) return if not blockName: return # 精确组合 (手持item, 被点block) pair = (itemName, blockName) # 盖饭/蛋炒饭(itemUseOn): 必须在此处理(比 ITEM_USE_ON_CANCEL 先), 否则会被 # ITEM_USE_ON_CANCEL 纯 ret 吞掉 -> 服务端 ServerItemUseOnEvent 不触发 -> 盖饭无反应。 if pair in GAI_FAN_CANCEL: args["ret"] = True self._afterCd[cdKey] = time.time() self.NotifyToServer(GAI_FAN_EVENT, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) return # 其余 itemUseOn 转换(面食/汤锅/装盘/厨具蘸油/寿司/裹粉): 纯 ret 会吞掉服务端 # ServerItemUseOnEvent -> 服务端 handleItemUseOn 不触发 -> "点无反应"(盖饭同款)。 # 统一改为 ret + 显式 ItemUseOnRequest, 由服务端按 itemName 路由补齐 handleItemUseOn。 if pair in ITEM_USE_ON_CANCEL: args["ret"] = True self._afterCd[cdKey] = time.time() self.NotifyToServer(ITEM_USE_ON_REQUEST, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) return # 汤锅盛出: 手持 碗/盘 右键 成品汤方块 -> 客户端取消放置预览(消闪), # 并显式通知服务端执行盛出(take_out)。此组合客户端拦掉后, # 服务端 ServerBlockUseEvent 不再触发, 故必须发 TakeoutRequest 补齐盛出逻辑。 if pair in TAKEOUT_CANCEL: args["ret"] = True self._afterCd[cdKey] = time.time() self.NotifyToServer(TAKEOUT_EVENT, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) return # 寿司: 手持寿司方块物品 右键 自家寿司方块 -> 服务端走 ShousiService.handleItemUseOn # (取消默认放置) + handleBlockUse(堆叠). 客户端 ret 吞事件后显式通知补齐。 if itemName in SUSHI_BLOCKS and blockName in SUSHI_BLOCKS: args["ret"] = True self._afterCd[cdKey] = time.time() self.NotifyToServer(ITEM_USE_ON_REQUEST, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) return # 裹粉: 手持生食材 右键 面粉块 -> 服务端走 ShicaiService.handleItemUseOn(裹粉). if itemName in COATING_ITEMS and blockName == "rtdd:farineblock_3": args["ret"] = True self._afterCd[cdKey] = time.time() self.NotifyToServer(ITEM_USE_ON_REQUEST, { "playerId": pId, "x": args.get("x"), "y": args.get("y"), "z": args.get("z"), "blockName": blockName, "itemName": itemName, }) # ============================================================================ # 入口3: 手持直接吃物品(长按/空中使用) -> cancel 默认吃 # ============================================================================ def OnItemTryUse(self, args): try: itemDict = args.get("itemDict") or {} itemName = itemDict.get("newItemName") or itemDict.get("itemName") except Exception: itemName = None _pId = args.get("playerId") or args.get("entityId") # 垃圾桶等"任意物品"方块: 玩家级 0.2s 窗口内直接取消伴随的"吃/用"。 # 不限物品名(该场景是"任意物品"被扔进垃圾桶/开盖, 而非吃它), 故不能用 # BLOCKUSE_CANCEL_ITEMS 判断, 单独用 _anyBlockPlayer 标记。 _ac = self._anyBlockPlayer.get(_pId, 0) if _ac and (time.time() - _ac) < 0.2: args["cancel"] = True return # 优先: 玩家刚做过 BlockUse 转换(0.2s 内, 如炒锅/汤锅/油锅放料、鸡蛋放鸡窝、上杆), # 引擎重置右键会立即再触发一次"吃/投掷/倒水"。此窗口内若手持该转换涉及的可食用/可用 # 物品(BLOCKUSE_CANCEL_ITEMS), 直接 cancel 掉默认吃/投掷, 避免"放料却吃了食物"。 # 玩家级标记(pId)而非坐标——TryUse 事件无目标方块坐标, 只有玩家。0.2s 极短且仅在 # 刚做完方块转换后生效, 不会影响玩家正常吃/对地面扔蛋。 _pc = self._blockCdPlayer.get(_pId, 0) if _pc and (time.time() - _pc) < 0.2 and itemName in BLOCKUSE_CANCEL_ITEMS: args["cancel"] = True return # 相邻放置窗口(0.2s): 手持原版食物右键方块表面(胡萝卜/土豆/干海带/生肉鱼/青菜/葱) # 放置时, 取消同一右键伴随的那次吃/用 —— 只在此窗口内生效。 # 空中/不对着方块右键 -> 没有 ItemUseOn 拦截 -> 没有标记 -> 正常吃原版食物。 if _pc and (time.time() - _pc) < 0.2 and itemName in PLACE_ITEMS: args["cancel"] = True return # 自定义直接吃物品(饭团/水煮蛋/豆腐, EAT_ITEMS): 永远取消默认吃, 走服务端自定义吃 if itemName in EAT_ITEMS: args["cancel"] = True # 注: 客户端系统需在客户端进程被实例化。modMain 只能在服务端 import 服务端 # 系统; 客户端系统由 BlueFox 框架自动注册其子类。为确保本类被加载, 需在 # modMain 客户端分支(IsInServer()==False)里 import 本模块。