Files

831 lines
46 KiB
Python
Raw Permalink 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 -*-
"""
================================================================================
统一事件分发器(服务端)—— 方案A核心
================================================================================
【事件监听在哪?—— 全模组只有这一个类监听游戏事件】
所有右键/计时相关事件都由本类(FeatureDispatcher)统一监听:
- ServerBlockUseEvent 玩家右键方块 → 按 blockOwner 路由
- ServerItemUseOnEvent 手持物品右键方块表面 → 按 itemUseOnOwner 路由
- ServerItemTryUseEvent 拦截喝药水/吃食物等默认行为(配合右键转换)
- ServerBlockEntityTickEvent 计时方块每tick → 按 timerOwner 路由
其他所有功能服务(AnimalService/BarbecueService 及各自的 TimerService)
【都不监听事件】。它们只是"逻辑处理类",被本类调用:
- 右键服务实现 handleBlockUse / handleItemUseOn,由本类转发
- 计时服务实现 handleBlockEntityTick / startTimer,由本类转发
【为什么要它】
以前每个功能都各自监听 ServerBlockUseEvent,玩家每次右键所有系统都被触发
一遍、各自跑一次数据库,功能越多损耗越大。
现在分发器用"方块名→服务"的 O(1) dict 路由,每个事件只命中一个服务。
【如何新增一个功能】
1. 新建 xxx/xxxData.py 写转换规则
2. 右键转换 → 新建 xxx/service/XxxService.py(继承 BlueFoxServerSystem):
- __init__ 里构建规则字典 + 调 FeatureDispatcher.register(self)
- 实现 getFeatureBlocks() / getItemUseOnRules() / handleBlockUse(...)
3. 计时转换 → 新建 xxx/service/XxxTimerService.py(继承 BlueFoxServerSystem):
- __init__ 里构建计时规则字典 + 调 FeatureDispatcher.registerTimer(self)
- 实现 getTimerBlocks() / handleBlockEntityTick(...) / startTimer(...)
- 右键服务要"启动计时"时,调 FeatureDispatcher.startTimerFor(...) 转发过来
4. 在 modMain.py import 这些服务类即可(框架会自动实例化)
================================================================================
"""
import time
import mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂
# ---- 继承 BlueFox 框架的系统基类 ----
from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ...BlueFoxTools.ToolsConfig import LevelId
from ...BlueFoxTools.ModConfig import MOD_NAME
# ---- 死亡生物·生成引擎: 玩家杀猪/牛/羊/鱿鱼/村民 -> 尸体方块 ----
# 它是"普通逻辑类"(自己不监听), 由本分发器实例化并把 ServerEntityDieEvent 转给它。
from ..siwangshengwu.service.DeadmobGeneratorService import DeadmobGeneratorService
# ---- 钓鱼掉落: 螃蟹/龙虾(普通逻辑类, 由本分发器转发 PlayerFishingServerEvent) ----
from ..yuanbanfangzhi.service.YuanbanFishing import YuanbanFishingService
# ---- 右键摆动动画(任务二): 服务器广播给该玩家客户端 -> Swing() ----
from ...ModUtil.ServerUtil import PlayAttackToClient
# ---- 乡野自然·水稻: 踩踏耕地等下方方块变化时 清理作物+掉稻谷 ----
from ..ziran.NaturalData import RICE_BLOCKS, RICE_SEED_ITEM, RICE_CROP_ITEM
# ---- 汤锅盛出自定义事件名(与客户端 ClientFeatureDispatcher TAKEOUT_EVENT 一致) ----
TAKEOUT_EVENT = "TakeoutRequest"
# ---- 盖饭/蛋炒饭 自定义事件名(与客户端 GAI_FAN_EVENT 一致) ----
GAI_FAN_EVENT = "GaifanRequest"
# ---- 通用"手持物品右键方块表面"转换请求(与客户端 ITEM_USE_ON_REQUEST 一致).
# 用于修复"客户端提前 ret=True 吞掉 ServerItemUseOnEvent 导致 itemUseOn 转换
# 点无反应"的问题(盖饭同款). 客户端对所有走 handleItemUseOn 的白名单组合,
# ret 掉放置预览后显式 Notify 本事件, 服务端按 itemName 路由到相应服务.
# 与 GaifanRequest 的区别: 本事件覆盖全部 itemUseOn 转换(面食/汤锅/装盘/
# 相邻放置/厨具蘸油/寿司/裹粉/种水稻), 而非仅盖饭. ----
ITEM_USE_ON_REQUEST = "ItemUseOnRequest"
# ---- 通用"手持物品右键功能方块"的 BlockUse 转换请求(与客户端 BLOCK_USE_REQUEST 一致).
# 用于修复根因B: 手持可放置/可投掷物品(生肉/鱼/鸡蛋/胡萝卜/盘子/木条/寿司等)右键
# 功能方块(支杆/挂肉钩/鸡窝/饲料槽/丸子机/石磨/炉灶/寿司方块)时, 原版先播放"放置/
# 投掷"预览, 服务端 handleBlockUse 转换靠 ServerBlockUseEvent 触发, 但顺序不可保证,
# 导致"想转换却放置/投掷了"。客户端对该(物品,功能方块)组合 ret 消闪后显式 Notify
# 本事件, 服务端按被点方块 blockOwnerAll 路由到相应 handleBlockUse (与 TakeoutRequest
# 同机制). ----
BLOCK_USE_REQUEST = "BlockUseRequest"
# ---- 方块实体数据组件(计时持久化:记录开始时间戳,重启后继续计时) ----
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
# ---- 方块表面方向 → 相邻坐标偏移(0下 1上 2北 3南 4西 5东) ----
# 用于放置后事件计算"落点"(被点方块的相邻格)
FACE_OFFSET = {
0: (0, -1, 0),
1: (0, 1, 0),
2: (0, 0, -1),
3: (0, 0, 1),
4: (-1, 0, 0),
5: (1, 0, 0),
}
# ---- 右键转换成功后"取消同一右键配套的放置/吃"的时间窗口(秒) ----
# 同一右键动作会同时触发 ServerBlockUseEvent(转换) 和 ServerItemUseOnEvent /
# ServerItemTryUseEvent(手持方块物品的放置前摇 / 喝药吃食)。OnBlockUse 转换成功后
# 会记录该玩家在此窗口内的后续 ItemUseOn/TryUse 应被取消(否则"转出了成品却又把
# 手持方块放出来")。超过窗口即视为过期残留, pop 后直接放行, 避免"做完一个操作
# 后任意位置放置都被吞"(统计: 空手右键只触发 BlockUse, 无配套 ItemUseOn 消费,
# 若标记无时限会永久残留吞掉玩家下一次真实放置)。
CANCEL_WINDOW = 0.5
class FeatureDispatcher(BlueFoxServerSystem):
"""
统一事件分发器(全模组唯一监听事件的地方)
【类级注册表(所有功能服务共享同一份路由表,O(1) 定位)】
blockOwner = {方块名: 右键服务实例} # ServerBlockUseEvent 用
itemUseOnOwner = {物品名: 右键服务实例} # ServerItemUseOnEvent 用
timerOwner = {方块名: 计时服务实例} # ServerBlockEntityTickEvent 用
"""
# ---- 类级共享路由表(用类变量,任何实例都能读写) ----
blockOwner = {}
blockSet = set()
blockOwnerAll = {} # 方块名 -> [服务...]: 同一方块可被多个服务注册(按注册顺序), 逐个 fallback
itemUseOnOwner = {}
itemUseOnSet = set()
itemUseOnAll = {} # 物品名 -> [服务...]: 同一物品可被多个服务注册(按注册顺序), 逐个 fallback
timerOwner = {}
timerSet = set()
tryUseOwner = {} # 物品名 -> 服务: 手持"直接吃"物品(长按使用)路由
tryUseSet = set()
grassDropOwners = [] # 破坏"草/杂草"时概率掉食材(如黄豆)的服务列表; OnDestroy 逐个尝试
# ---- 位置级"防误吃"窗口: (dim, x, y, z) -> 到期时间戳 ----
# 转换服务刚把方块换成"可吃成品"(如翻烤出炉的熟串)后, 给该位置加一小段禁吃时间。
# 原因: 翻烤(BarbecueService)与吃(ShiyongfoodService)是两种服务、两张独立冷却表,
# 而输入都是"空手右键同一位置"; 快速连点时翻烤出锅后的下一次点击会直接进吃逻辑,
# 把刚烤好的串立刻吃掉。窗口内空手右键不执行吃(见 ShiyongfoodService.handleBlockUse)。
eatLock = {}
EAT_LOCK_SECONDS = 1.0
# ---- 拆方块掉落映射: 方块名 → 掉落对应的原版生食材 ----
# 玩家左键破坏这些"放置的半成品/死鱼"方块时, 取消默认掉落, 改为掉对应的原版生食材
DESTROY_DROP = {
"rtdd:killfish_0": "minecraft:salmon", # 死鲑鱼 → 原版生鲑鱼
"rtdd:killcod_0": "minecraft:cod", # 死鳕鱼 → 原版生鳕鱼
"rtdd:roastchickenplace_0": "minecraft:chicken", # 生鸡肉(放置后) → 原版生鸡肉
"rtdd:sushi_0": "minecraft:dried_kelp", # 干海带(放置后) → 原版干海带
}
# ============================================================================
# 注册接口(功能服务在 __init__ 里调用)
# ============================================================================
@classmethod
def register(cls, service):
"""
右键服务初始化时调用:把管辖的"右键方块"和"手持物品右键方块表面"注册进路由表。
同一方块/物品可被多个服务注册(如 面粉块: 食材裹粉去重 + 面食和面),
按注册顺序追加, OnBlockUse/OnItemUseOn 逐个 fallback(第一个返回 True 的处理)。
"""
# 右键方块路由:方块名 → 服务列表
for block in service.getFeatureBlocks():
cls.blockOwner[block] = service
cls.blockSet.add(block)
cls.blockOwnerAll.setdefault(block, []).append(service)
# 手持物品右键方块表面路由:物品名 → 服务
for itemName in service.getItemUseOnRules():
cls.itemUseOnOwner[itemName] = service
cls.itemUseOnSet.add(itemName)
# 同一物品可被多个服务注册(如 食材裹粉 vs 原版放置 都处理 土豆),
# 按注册顺序追加, OnItemUseOn 逐个 fallback
cls.itemUseOnAll.setdefault(itemName, []).append(service)
@classmethod
def registerTimer(cls, service):
"""
计时服务初始化时调用:把管辖的"计时源方块"注册进计时路由表。
ServerBlockEntityTickEvent 会按此表把 tick 事件路由到对应的计时服务。
"""
for block in service.getTimerBlocks():
cls.timerOwner[block] = service
cls.timerSet.add(block)
@classmethod
def registerTryUse(cls, service):
"""
手持"直接吃"服务初始化时调用:把"手持右键直接吃(不放置)"的物品注册进 try-use
路由表。ServerItemTryUseEvent(长按使用物品) 会按此表把事件路由到对应服务。
"""
for item in service.getTryUseRules():
cls.tryUseOwner[item] = service
cls.tryUseSet.add(item)
@classmethod
def registerGrassDrop(cls, service):
"""
破坏草/杂草掉食材(如黄豆)的服务初始化时调用:把自己挂进 grassDropOwners。
OnDestroy 遇到草/杂草方块会逐个调用这些服务的 handleGrassDrop(args)。
"""
if service not in cls.grassDropOwners:
cls.grassDropOwners.append(service)
@classmethod
def startTimerFor(cls, blockName, dim, pos, playerId=None):
"""
右键服务调用:把"计时源方块"的计时启动任务转发给对应的计时服务。
- 例如:烧烤右键"烤鸡(生)"开始烤制;动物装满饲料盆后自动开始变空计时
- 内部通过 timerOwner 路由表 O(1) 找到计时服务,调它的 startTimer,
并返回计时服务的实际返回值:
True = 本次真正启动了计时
False = 没有计时服务 / 该方块已在计时中(不重复启动)
"""
service = cls.timerOwner.get(blockName)
if not service:
return False
return service.startTimer(dim, pos, blockName, playerId)
@classmethod
def lockEat(cls, pos, dim, seconds=None):
"""
转换服务调用:给"刚转换出来的方块位置"加一个防误吃窗口。
窗口内空手右键该位置不会执行"吃"(见 ShiyongfoodService.handleBlockUse),
避免快速连点把刚出锅的可吃成品(如熟串)立刻吃掉。
"""
if seconds is None:
seconds = cls.EAT_LOCK_SECONDS
cls.eatLock[(dim, pos[0], pos[1], pos[2])] = time.time() + seconds
@classmethod
def isEatLocked(cls, pos, dim):
"""查询该位置的"防误吃"窗口是否仍在生效(过期自动视为未锁定)"""
return time.time() < cls.eatLock.get((dim, pos[0], pos[1], pos[2]), 0)
def __init__(self, namespace, systemName):
"""
初始化:注册事件监听(全模组唯一监听点)。
注意:本类自身不注册任何功能方块,只负责把事件分发给已注册的服务。
"""
super(FeatureDispatcher, self).__init__(namespace, systemName)
print "===== FeatureDispatcher init ====="
self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等)
self._deadmobGen = DeadmobGeneratorService() # 死亡生物·生成引擎(杀猪牛羊等->尸体方块)
self._fishing = YuanbanFishingService() # 钓鱼掉落(螃蟹/龙虾)
self.ListenEvent()
def Destroy(self):
print "===== FeatureDispatcher Destroy ====="
self.UnListenEvent()
# ============================================================================
# 事件注册与注销(这是全模组唯一监听事件的地方)
# ============================================================================
def ListenEvent(self):
"""
注册七个事件:
1. ServerBlockUseEvent —— 玩家右键方块(所有功能的右键转换入口)
2. ServerItemUseOnEvent —— 手持物品右键方块表面(如放置生鸡肉)
3. ServerItemTryUseEvent —— 玩家尝试使用物品(拦截喝药水/吃食物等默认行为)
4. ServerBlockEntityTickEvent —— 计时方块每tick触发(烤制/饲料变空等,持久化计时)
5. ServerPlayerTryDestroyBlockEvent —— 玩家拆方块(放置的死鱼/生鸡肉掉回原版生食材;
以及草/杂草 概率掉黄豆→ 转给 shicai 的 handleGrassDrop)
6. ItemUseOnAfterServerEvent —— 放置方块/生态后触发(携带放置者 entityId)。
用于"创造模式直接放生态"也能拿到是谁放的 → 记录到计时源方块 rtdd_tp,
让计时源服务能"谁放提示发给谁"(如烧烤直接放烤乳猪也提示营火)。
7. MobDieEvent —— 生物死亡(玩家杀死猪/牛/羊/鱿鱼/村民 →
转给 _deadmobGen 生成尸体方块)。注意: 网易该 SDK 里死亡事件叫 MobDieEvent
(参数 id=死亡实体, attacker=击杀者), 不是 ServerEntityDieEvent。
"""
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent", self, self.OnBlockUse)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerItemUseOnEvent", self, self.OnItemUseOn)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerItemTryUseEvent", self, self.OnItemTryUse)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockEntityTickEvent", self, self.OnBlockEntityTick)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerPlayerTryDestroyBlockEvent", self, self.OnDestroy)
# 水稻/自定义方块周围方块变化(踩踏耕地使下方变泥土/草地等) -> 清理作物+掉稻谷
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"BlockNeighborChangedServerEvent", self, self.OnBlockNeighborChanged)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ItemUseOnAfterServerEvent", self, self.OnItemPlaceAfter)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"MobDieEvent", self, self.OnEntityDie)
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"PlayerFishingServerEvent", self, self.OnFishing)
# 7. TakeoutRequest —— 客户端拦截(碗/盘,汤锅/炒锅/油锅成品)放置预览后, 显式请求服务端盛出。
# 客户端系统 ClientFeatureDispatcher 用 NotifyToServer(TAKEOUT_EVENT) 发送,
# 服务端以 MOD_NAME + 客户端系统名 监听该自定义事件。
self.ListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
TAKEOUT_EVENT, self, self.OnTakeoutRequest)
# 8. GaifanRequest —— 客户端拦截(成品菜/米饭,米饭/成品菜)盖饭放置预览后, 显式请求转换。
# 同盛出: 必须显式触发, 否则客户端 ret=True 吞掉 ServerItemUseOnEvent, 盖饭无反应。
self.ListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
GAI_FAN_EVENT, self, self.OnGaifanRequest)
# 9. ItemUseOnRequest —— 客户端拦截其余全部 itemUseOn 转换(面食/汤锅/装盘/相邻放置/
# 厨具蘸油/寿司/裹粉/种水稻)的放置预览后, 显式请求服务端执行对应 handleItemUseOn。
# (与 GaifanRequest 同机制, 统一覆盖全部 itemUseOn 转换)
self.ListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
ITEM_USE_ON_REQUEST, self, self.OnItemUseOnRequest)
# 10. BlockUseRequest —— 客户端拦截(手持可放置/可投掷物品, 功能方块)放置预览后,
# 显式请求服务端执行对应 handleBlockUse(根因B: 防止"想转换却放置/投掷"了)。
self.ListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
BLOCK_USE_REQUEST, self, self.OnBlockUseRequest)
def UnListenEvent(self):
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent", self, self.OnBlockUse)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerItemUseOnEvent", self, self.OnItemUseOn)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerItemTryUseEvent", self, self.OnItemTryUse)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockEntityTickEvent", self, self.OnBlockEntityTick)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerPlayerTryDestroyBlockEvent", self, self.OnDestroy)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"BlockNeighborChangedServerEvent", self, self.OnBlockNeighborChanged)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ItemUseOnAfterServerEvent", self, self.OnItemPlaceAfter)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"MobDieEvent", self, self.OnEntityDie)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"PlayerFishingServerEvent", self, self.OnFishing)
self.UnListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
TAKEOUT_EVENT, self, self.OnTakeoutRequest)
self.UnListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
GAI_FAN_EVENT, self, self.OnGaifanRequest)
self.UnListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
ITEM_USE_ON_REQUEST, self, self.OnItemUseOnRequest)
self.UnListenForEvent(MOD_NAME, "ClientFeatureDispatcher",
BLOCK_USE_REQUEST, self, self.OnBlockUseRequest)
# ============================================================================
# 主入口1:玩家右键方块 → 按 blockOwner 路由到右键服务
# ============================================================================
def OnBlockUse(self, args):
"""
玩家右键方块时触发。
用被右键的方块名 O(1) 定位到注册它的所有功能服务(blockOwnerAll, 按注册顺序),
逐个尝试 handleBlockUse, 第一个返回 True 的服务处理成功(多服务 fallback,
如 面粉块: 食材服务先试(裹粉去重, 空手无规则返回False) -> 面食服务和面)。
服务返回 True = 处理成功 → 取消默认行为,并标记拦截下一次物品使用
服务返回 False = 当前组合无匹配规则 → 交还默认行为
"""
blockName = args.get("blockName")
playerId = args.get("playerId")
# 手持物品名(用于区分空手/木碗/盘子)
try:
_itemDict = self.getCarriedItem(playerId)
_handItem = _itemDict.get("newItemName") if _itemDict else "NONE"
except Exception:
_handItem = "NONE"
services = self.blockOwnerAll.get(blockName)
print "[FD-BlockUse-ENTER]", "player:", playerId, "hand:", _handItem, "block:", blockName, "svcs:", ([s.__class__.__name__ for s in services] if services else "NONE")
if not services:
# 这个方块不属于任何已注册的功能 → 交给原版/其他模组处理
return
for service in services:
handled = service.handleBlockUse(args)
if handled:
args['cancel'] = True
# 记录时间戳(带窗口), 只取消同一右键配套的放置/吃; 过期残留自动放行
self._cancelItemUse[playerId] = time.time()
PlayAttackToClient(playerId) # 任务二: 右键动作播放摆动
print "[FD-BlockUse-DONE]", "player:", playerId, "hand:", _handItem, "block:", blockName, "svc:", service.__class__.__name__, "-> cancel=True, _cancelItemUse=ts"
return
print "[FD-BlockUse-NOMATCH]", "player:", playerId, "hand:", _handItem, "block:", blockName, "-> 无匹配规则, 放行"
# ============================================================================
# 主入口2:手持物品右键方块表面 → 按 itemUseOnOwner 路由到右键服务
# ============================================================================
def OnItemUseOn(self, args):
"""
手持物品右键某个方块表面时触发(如手持原版生鸡肉右键地面)。
用手持物品名定位到注册该物品的【所有】服务(itemUseOnAll,按注册顺序),
逐个尝试 handleItemUseOn,第一个返回 True 的服务处理成功:
- 食材裹粉(ShicaiService) 与 原版放置(YuanbanService) 都注册了 土豆/鸡肉/猪肉,
手持这些物品右键面粉块 → 裹粉先命中; 右键地面 → 裹粉返回 False 后交给原版放置。
取消"使用物品于方块"的字段是 ret(不是 cancel),官方要求双端设置才能
真正取消默认行为。本分发器已在服务端设 ret=True。
"""
playerId = args.get("playerId") or args.get("entityId")
# 【关键】同一右键动作会同时触发 ServerBlockUseEvent(盛出/转换, 已由 OnBlockUse
# 处理并设置 _cancelItemUse 时间戳) 和 ServerItemUseOnEvent(手持方块物品的放置前摇)。
# 若在 CANCEL_WINDOW 窗口内, 这里必须消费标记并 ret=True 取消放置, 否则"盛出了成品"
# 却又"把手持的木碗/盘子放了出来"。超窗口则视为过期残留, pop 后直接放行——绝不吞
# 玩家之后的真实放置(日志: 空手右键完成后残留标记吞掉任意位置放置, 已修复)。
_ts = self._cancelItemUse.pop(playerId, 0)
if _ts and (time.time() - _ts) < CANCEL_WINDOW:
args['ret'] = True
print "[FD-ItemUseOn-CONSUME-CANCEL]", "player:", playerId, "item:", args.get("itemName"), "block:", args.get("blockName"), "-> ret=True 取消放置"
return
itemName = args.get("itemName")
services = self.itemUseOnAll.get(itemName)
# 被点方块(用于去重: 该方块归哪些右键服务管辖)
clickedName = None
try:
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
b = comp.CreateBlockInfo(LevelId).GetBlockNew(
(args.get("x"), args.get("y"), args.get("z")), dim)
clickedName = b.get("name") if b else None
except Exception:
pass
owners = self.blockOwnerAll.get(clickedName) if clickedName else None
# 【已移除宽泛兜底】以前"手持方块物品右键任何功能方块且无 itemUseOn 服务
# 就 ret=True 取消默认放置", 导致正常放置失效。现在改由客户端白名单精确
# 拦截(见 ClientFeatureDispatcher + InterceptData), 服务端只处理真正注册了
# itemUseOn 的组合, 其余一律放行(放置恢复正常)。
if not services:
return
print "[FD-ItemUseOn]", itemName, "svcs:", [s.__class__.__name__ for s in services]
for service in services:
# 被点方块属于某些"右键方块"服务的管辖(条箱/挂肉钩/锅具等) → 该方块右键由
# ServerBlockUseEvent 处理(放入/挂鱼/做菜), 跳过本服务的 itemUseOn,
# 避免与方块右键逻辑重复(如手持鲑鱼右键条箱应放入而非放置死鲑鱼)。
# 例外: 当前服务本身就在被点方块的归属列表里(如 食材裹粉: 面粉块归
# ShicaiService+面食服务 管辖, 手持生食材右键它时放行给 handleItemUseOn
# 处理) 则继续尝试。
# 【额外例外】服务声明了 ownsItemUseOn(itemName, clickedName) 且返回 True,
# 说明该(手持物品,被点方块)组合明确由本服务的 itemUseOn 规则处理(如炒锅盖饭:
# 成品菜/米饭尽管也被"总食用"登记为右键吃方块, 但盖饭转换应仍走 itemUseOn)。
# 此时不因被点方块归属别的服务而跳过, 否则盖饭永不被触发。
if clickedName and owners and service not in owners:
_owns = None
try:
_owns = service.ownsItemUseOn(itemName, clickedName)
except Exception:
_owns = None
if not _owns:
continue
handled = service.handleItemUseOn(args)
if handled:
args['ret'] = True
# 时间戳(带窗口): itemUseOn 转换成功后, 同右手的后续 TryUse/放置也取消
self._cancelItemUse[playerId] = time.time()
PlayAttackToClient(playerId) # 任务二: 右键动作播放摆动
return
# ============================================================================
# 主入口2.5:放置方块/生态后 → 给计时源补记录放置者(谁放提示发给谁)
# ============================================================================
def OnItemPlaceAfter(self, args):
"""
玩家放置方块/生态后触发(ItemUseOnAfterServerEvent)。
用途:创造模式"直接放烤乳猪等计时源生态"时,没有右键上杆,拿不到放置者。
这里拿到 放置者(entityId) + 落点方块,若落点是某个计时源的方块,
就带 playerId 调 startTimerFor → startTimer 会判断下方营火并提示"谁放的"。
这样"创造直接放"也能做到谁放提示发给谁(不依赖右键路径)。
"""
playerId = args.get("entityId")
if not playerId:
return
# 被点方块的坐标 + 朝向面
try:
face = args.get("face", 1)
x, y, z = args.get("x"), args.get("y"), args.get("z")
except Exception:
return
# 落点 = 被点方块相邻的那一格(0下 1上 2北 3南 4西 5东)
off = FACE_OFFSET.get(face, (0, 1, 0))
targetPos = (x + off[0], y + off[1], z + off[2])
dim = args.get("dimensionId")
if dim is None:
try:
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
except Exception:
return
# 读取落点方块名
try:
b = comp.CreateBlockInfo(LevelId).GetBlockNew(targetPos, dim)
blockName = b.get("name") if b else ""
except Exception:
return
if not blockName:
return
print "[FD-Placed-AFTER]", "player:", playerId, "placedBlock:", blockName, "targetPos:", targetPos
# 落点是计时源方块 → 带放置者启动(判断营火 + 谁放提示发给谁)
service = self.timerOwner.get(blockName)
if service and hasattr(service, "startTimer"):
service.startTimer(dim, targetPos, blockName, playerId)
# ============================================================================
# 主入口2.6:客户端显式触发的"盛出"请求(汤锅 take_out 专用,消闪方案)
# ============================================================================
def OnTakeoutRequest(self, args):
"""
客户端 ClientFeatureDispatcher 在拦截(碗/盘, 汤锅成品)放置预览后,
显式 NotifyToServer 发来的盛出请求。因为客户端已 ret=True 把该组合的
ServerBlockUseEvent 吞掉(消闪), 所以盛出逻辑必须在这里手动补齐。
与服务器普通右键一致, 需要校验: 别再重复执行, 关键落在 handleTakeoutRequest
里"落点仍是成品方块才执行"的守卫上(防客户端事件与服务端事件双份)。
"""
playerId = args.get("playerId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
blockName = args.get("blockName")
itemName = args.get("itemName")
if not (playerId and x is not None and y is not None and z is not None and blockName):
return
try:
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
except Exception:
return
services = self.blockOwnerAll.get(blockName)
if not services:
return
for service in services:
if hasattr(service, "handleTakeoutRequest"):
# handleTakeoutRequest 内部已校验"落点仍是成品方块" + 冷却, 防双份。
# 盛出后引擎"把手持碗/盘放到已变空锅"的多放置闪烁由客户端 CD(1.5秒)
# 在客户端吞掉, 服务端不再设 _cancelItemUse(否则标记残留会吞掉玩家
# 下一次真实的 ServerItemUseOnEvent, 影响其它操作)。
service.handleTakeoutRequest(playerId, blockName, itemName, dim, (x, y, z))
PlayAttackToClient(playerId) # 任务二: 右键动作播放摆动
return
print "[FD-TakeoutRequest-NO-HANDLER]", blockName
def OnGaifanRequest(self, args):
"""
客户端 ClientFeatureDispatcher 在拦截(成品菜/米饭,米饭/成品菜)放置预览后,
显式 NotifyToServer 发来的盖饭/蛋炒饭转换请求。客户端已 ret=True 把该组合的
ServerItemUseOnEvent 吞掉(消闪), 这里必须显式补齐转换逻辑(否则盖饭"点无反应")。
构造 itemUseOn 所需的 args, 按 itemName 路由到 itemUseOn 服务(ChaoguoService)。
"""
playerId = args.get("playerId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
itemName = args.get("itemName")
if not (playerId and x is not None and y is not None and z is not None and itemName):
return
services = self.itemUseOnAll.get(itemName)
if not services:
print "[FD-GaifanRequest-NO-SVC]", itemName
return
# 复用服务端的 itemUseOn 参数结构(handleItemUseOn 读取 itemName/playerId/x/y/z)
useOnArgs = {
"itemName": itemName,
"playerId": playerId,
"entityId": playerId,
"x": x, "y": y, "z": z,
"blockName": args.get("blockName"),
}
for service in services:
if hasattr(service, "handleItemUseOn"):
# 盖饭是本方块原位转换成盖饭(消耗手持成品菜/米饭), 不是"放置手持方块",
# 因此不设 _cancelItemUse —— 否则标记残留会吞掉玩家下一次真实的 itemUseOn.
handled = service.handleItemUseOn(useOnArgs)
if handled:
PlayAttackToClient(playerId) # 任务二: 右键动作播放摆动
return
print "[FD-GaifanRequest-NO-HANDLER]", itemName
def OnItemUseOnRequest(self, args):
"""
客户端 ClientFeatureDispatcher 在拦截【所有走 handleItemUseOn 的 itemUseOn 转换】
(面食/汤锅/装盘/相邻放置/厨具蘸油/寿司/裹粉/种水稻) 放置预览后, 显式 NotifyToServer
发来的通用转换请求. 因为客户端已 ret=True 吞掉该组合的 ServerItemUseOnEvent(消闪),
服务端原本会被吞掉的 itemUseOn 转换在这里补齐路由(与 OnGaifanRequest 同机制).
按 itemName 路由到 itemUseOnAll 里的服务(可能有多个, 如 裹粉Shicai + 原版放置Yuanban
都处理 土豆/鸡肉/猪肉, 逐个 fallback, 第一个返回 True 的处理成功).
注意: 各服务 handleItemUseOn 内部自带 checkCooldown(同坐标/同物品), 即使与真实的
ServerItemUseOnEvent 重复触发, 冷却也会拦截第二次, 不会双重消耗/双重转换.
"""
playerId = args.get("playerId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
itemName = args.get("itemName")
if not (playerId and x is not None and y is not None and z is not None and itemName):
return
services = self.itemUseOnAll.get(itemName)
if not services:
print "[FD-ItemUseOnRequest-NO-SVC]", itemName
return
# 复用服务端的 itemUseOn 参数结构(handleItemUseOn 读取 itemName/playerId/x/y/z)
useOnArgs = {
"itemName": itemName,
"playerId": playerId,
"entityId": playerId,
"x": x, "y": y, "z": z,
"blockName": args.get("blockName"),
}
for service in services:
if hasattr(service, "handleItemUseOn"):
handled = service.handleItemUseOn(useOnArgs)
if handled:
PlayAttackToClient(playerId) # 任务二: 右键动作播放摆动
return
print "[FD-ItemUseOnRequest-NO-HANDLER]", itemName
def OnBlockUseRequest(self, args):
"""
客户端 ClientFeatureDispatcher 在拦截(手持可放置/可投掷物品, 功能方块)放置预览后,
显式 NotifyToServer 发来的 BlockUse 转换请求(根因B). 因为客户端已 ret=True 把该
组合的 ServerBlockUseEvent 吞掉(消闪), 服务端原本的 handleBlockUse 转换在这里补齐.
按被点方块名路由到 blockOwnerAll 里的服务(可能多个, 逐个 fallback, 第一个返回 True
的处理成功, 与 OnBlockUse 相同). 各服务 handleBlockUse 内部自带 checkCooldown(同坐标/
同物品), 即使与真实 ServerBlockUseEvent 重复触发, 冷却也会拦截第二次, 不双重消耗.
"""
playerId = args.get("playerId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
blockName = args.get("blockName")
if not (playerId and x is not None and y is not None and z is not None and blockName):
return
services = self.blockOwnerAll.get(blockName)
if not services:
print "[FD-BlockUseRequest-NO-SVC]", blockName
return
useArgs = {
"blockName": blockName,
"playerId": playerId,
"entityId": playerId,
"x": x, "y": y, "z": z,
"face": args.get("face", 1),
}
for service in services:
if hasattr(service, "handleBlockUse"):
handled = service.handleBlockUse(useArgs)
if handled:
PlayAttackToClient(playerId) # 任务二: 右键动作播放摆动
return
print "[FD-BlockUseRequest-NO-HANDLER]", blockName
# ============================================================================
# 主入口3:拦截物品使用(防止喝药水/吃食物等默认行为)
# ============================================================================
def OnItemTryUse(self, args):
"""
玩家尝试使用物品(喝药水、吃食物等)时触发。
1. 如果该玩家刚被标记为"取消下一次物品使用"(_cancelItemUse),
就取消这次默认行为,避免右键转换方块时误喝药水。
2. 否则按 tryUseOwner 路由:手持"直接吃"物品(饭团/水煮蛋/豆腐) -> 吃掉。
"""
playerId = args.get("playerId")
print "[FD-ItemTryUse]", args.get("itemName"), "player", playerId
# 仅在 CANCEL_WINDOW 窗口内消费(同一右键配套的吃/喝); 过期残留 pop 后放行,
# 不吞玩家之后再吃/喝物品(根因A: 无时限标记残留会吞掉后续任意物品使用)。
_ts = self._cancelItemUse.pop(playerId, 0)
if _ts and (time.time() - _ts) < CANCEL_WINDOW:
args['cancel'] = True
return
itemName = args.get("itemName")
service = self.tryUseOwner.get(itemName)
if service and service.handleItemTryUse(args):
args['cancel'] = True
PlayAttackToClient(playerId) # 任务二: 手持右键动作播放摆动
# ============================================================================
# 主入口4:计时方块 tick → 按 timerOwner 路由到计时服务(持久化计时)
# ============================================================================
def OnBlockEntityTick(self, args):
"""
配置了 tick:true 的方块每帧触发(官方频率约每秒20次)。
用于"烤制/饲料变空"等计时转换的持久化:
- 转换开始时把 开始时间戳+时长 写进方块实体数据
- 服务器重启/玩家重进后,tick 恢复,读回时间戳继续计时,
到点即转换(例如烤鸡烤了40秒退出,回来只剩20秒)
路由:用事件自带的 blockName 在 timerOwner 里 O(1) 找到计时服务,
转发给它的 handleBlockEntityTick(省掉每帧读方块,性能更好)。
注意:事件参数是 blockName/dimension/posX/posY/posZ(不是 posDim!)
"""
posX = args.get("posX")
posY = args.get("posY")
posZ = args.get("posZ")
dimension = args.get("dimension")
if posX is None or posY is None or posZ is None or dimension is None:
return
blockName = args.get("blockName", "")
service = self.timerOwner.get(blockName) # 计时源方块一定注册在 timerOwner
if not service:
return
service.handleBlockEntityTick(args)
# ============================================================================
# 主入口5:玩家拆方块 → 放置的死鱼/生鸡肉掉回原版生食材
# ============================================================================
def OnEntityDie(self, args):
"""
玩家杀死生物时触发(ServerEntityDieEvent)。
转给死亡生物·生成引擎: 若击杀者是玩家且被杀是 猪/牛/羊/鱿鱼/村民,
在死亡位置生成对应尸体方块。
"""
try:
self._deadmobGen.OnEntityDie(args)
except Exception as e:
print "[FD-OnEntityDie-ERR]", e
def OnFishing(self, args):
"""
玩家钓鱼收杆时触发(PlayerFishingServerEvent)。
转给 原版放置·钓鱼掉落(YuanbanFishingService): 取消默认钓物并在鱼钩位置
生成 螃蟹/龙虾。
"""
try:
self._fishing.OnFishing(args)
except Exception as e:
print "[FD-OnFishing-ERR]", e
def OnDestroy(self, args):
"""
玩家拆方块时触发。
注意: ServerPlayerTryDestroyBlockEvent 的方块名字段是 fullName(不是 blockName)!
优先级:
1. 有"破坏掉落"逻辑的服务(如家具的存物方块: 书柜/药水架/厨具架
被拆掉 → 掉 空方块本体+全部存储物品, 与售卖盒不同, 破坏后取不到)
2. 静态映射: 放置的死鱼/生鸡肉掉回原版生食材
"""
blockName = args.get("fullName") or args.get("blockName")
args["blockName"] = blockName # 统一成 blockName 字段, 服务内部直接用
# 0. 草/杂草 概率掉食材(黄豆): 逐个尝试挂了 handleGrassDrop 的服务。
# 如 shicai: 打草概率掉黄豆, 不取消破坏本身。
if self.grassDropOwners:
for _s in self.grassDropOwners:
if hasattr(_s, "handleGrassDrop") and _s.handleGrassDrop(args):
return
# 1. 优先: 归属的右键服务实现了 handleDestroy → 交给服务处理(逐个 fallback)
services = self.blockOwnerAll.get(blockName)
if services:
for service in services:
if hasattr(service, "handleDestroy"):
if service.handleDestroy(args):
return
# 2. 静态映射: 放置的死鱼/生鸡肉 → 原版生食材
itemName = self.DESTROY_DROP.get(blockName)
if not itemName:
return
playerId = args.get("playerId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
# 取消默认破坏+默认掉落(不让方块本身作为掉落物掉出来)
args['cancel'] = True
args['spawnResources'] = False
# 手动清空方块
try:
comp.CreateBlockInfo(LevelId).SetBlockNew(
(x, y, z), blockDict={"name": "minecraft:air", "aux": 0},
oldBlockHandling=0, dimensionId=dim)
except Exception:
pass
# 掉落对应的原版生食材
try:
itemDict = {"itemName": itemName, "count": 1, "auxValue": 0}
self.CreateEngineItemEntity(itemDict, dim, (x, y + 0.5, z))
except Exception:
pass
def OnBlockNeighborChanged(self, args):
"""
BlockNeighborChangedServerEvent —— 自定义方块周围方块变化时触发。
用途: 稻谷(riceseedlingsb/c/d)下方是耕地(farmland)时, 一旦耕地被踩踏/替换/推动
(变成泥土/草方块等), 原版会把作物连根。为保证"踩耕地→作物消失且掉稻谷"(跟原版
一致, 而不掉方块本体), 这里在下方耕地变化的瞬间:
- 若稻谷方块仍存在于原地, 手动清空它;
- 并掉落稻谷(b/c 掉1; 成熟 d 掉 稻谷1 + 大米16)。
只处理稻谷块; 其余方块直接放行。
"""
blockName = args.get("blockName")
if blockName not in RICE_BLOCKS:
return
posX, posY, posZ = args.get("posX"), args.get("posY"), args.get("posZ")
dim = args.get("dimensionId")
# 仅当下方(正下方一格)方块变化才处理, 避免同层/上方邻居变化误清
if (args.get("neighborPosX") != posX or
args.get("neighborPosZ") != posZ or
args.get("neighborPosY") != (posY - 1)):
return
try:
below = comp.CreateBlockInfo(LevelId).GetBlockNew((posX, posY - 1, posZ), dim)
belowName = below.get("name") if below else None
except Exception:
belowName = None
# 下方仍是耕地 -> 不算失去支撑, 放行
if belowName == "minecraft:farmland":
return
# 清空作物方块(取消默认掉落, 手动掉)
try:
comp.CreateBlockInfo(LevelId).SetBlockNew(
(posX, posY, posZ), blockDict={"name": "minecraft:air", "aux": 0},
oldBlockHandling=0, dimensionId=dim)
except Exception:
pass
try:
self.CreateEngineItemEntity(
{"itemName": RICE_SEED_ITEM, "count": 1, "auxValue": 0},
dim, (posX, posY + 0.5, posZ))
if blockName == "rtdd:riceseedlingsd":
self.CreateEngineItemEntity(
{"itemName": RICE_CROP_ITEM, "count": 16, "auxValue": 0},
dim, (posX, posY + 0.5, posZ))
except Exception:
pass