Files
rtdd/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/service/ClientFeatureDispatcher.py
T
2026-09-30 12:38:46 +08:00

291 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 本模块。