Files
rtdd/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/xiangyemod/service/FeatureDispatcher.py
T
2026-09-11 11:51:23 +08:00

365 lines
19 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 -*-
"""
================================================================================
统一事件分发器(服务端)—— 方案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 mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂
# ---- 继承 BlueFox 框架的系统基类 ----
from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ...BlueFoxTools.ToolsConfig import LevelId
# ---- 方块实体数据组件(计时持久化:记录开始时间戳,重启后继续计时) ----
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
class FeatureDispatcher(BlueFoxServerSystem):
"""
统一事件分发器(全模组唯一监听事件的地方)
【类级注册表(所有功能服务共享同一份路由表,O(1) 定位)】
blockOwner = {方块名: 右键服务实例} # ServerBlockUseEvent 用
itemUseOnOwner = {物品名: 右键服务实例} # ServerItemUseOnEvent 用
timerOwner = {方块名: 计时服务实例} # ServerBlockEntityTickEvent 用
"""
# ---- 类级共享路由表(用类变量,任何实例都能读写) ----
blockOwner = {}
blockSet = set()
blockOwnerAll = {} # 方块名 -> [服务...]: 同一方块可被多个服务注册(按注册顺序), 逐个 fallback
itemUseOnOwner = {}
itemUseOnSet = set()
itemUseOnAll = {} # 物品名 -> [服务...]: 同一物品可被多个服务注册(按注册顺序), 逐个 fallback
timerOwner = {}
timerSet = set()
# ---- 拆方块掉落映射: 方块名 → 掉落对应的原版生食材 ----
# 玩家左键破坏这些"放置的半成品/死鱼"方块时, 取消默认掉落, 改为掉对应的原版生食材
DESTROY_DROP = {
"rtdd:killfish_0": "minecraft:salmon", # 死鲑鱼 → 原版生鲑鱼
"rtdd:killcod_0": "minecraft:cod", # 死鳕鱼 → 原版生鳕鱼
"rtdd:roastchickenplace_0": "minecraft:chicken", # 生鸡肉(放置后) → 原版生鸡肉
}
# ============================================================================
# 注册接口(功能服务在 __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 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)
def __init__(self, namespace, systemName):
"""
初始化:注册事件监听(全模组唯一监听点)。
注意:本类自身不注册任何功能方块,只负责把事件分发给已注册的服务。
"""
super(FeatureDispatcher, self).__init__(namespace, systemName)
print "===== FeatureDispatcher init ====="
self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等)
self.ListenEvent()
def Destroy(self):
print "===== FeatureDispatcher Destroy ====="
self.UnListenEvent()
# ============================================================================
# 事件注册与注销(这是全模组唯一监听事件的地方)
# ============================================================================
def ListenEvent(self):
"""
注册五个事件:
1. ServerBlockUseEvent —— 玩家右键方块(所有功能的右键转换入口)
2. ServerItemUseOnEvent —— 手持物品右键方块表面(如放置生鸡肉)
3. ServerItemTryUseEvent —— 玩家尝试使用物品(拦截喝药水/吃食物等默认行为)
4. ServerBlockEntityTickEvent —— 计时方块每tick触发(烤制/饲料变空等,持久化计时)
5. ServerPlayerTryDestroyBlockEvent —— 玩家拆方块(放置的死鱼/生鸡肉掉回原版生食材)
"""
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)
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)
# ============================================================================
# 主入口1:玩家右键方块 → 按 blockOwner 路由到右键服务
# ============================================================================
def OnBlockUse(self, args):
"""
玩家右键方块时触发。
用被右键的方块名 O(1) 定位到注册它的所有功能服务(blockOwnerAll, 按注册顺序),
逐个尝试 handleBlockUse, 第一个返回 True 的服务处理成功(多服务 fallback,
如 面粉块: 食材服务先试(裹粉去重, 空手无规则返回False) -> 面食服务和面)。
服务返回 True = 处理成功 → 取消默认行为,并标记拦截下一次物品使用
服务返回 False = 当前组合无匹配规则 → 交还默认行为
"""
blockName = args.get("blockName")
services = self.blockOwnerAll.get(blockName)
print "[FD-BlockUse]", 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
playerId = args.get("playerId")
self._cancelItemUse[playerId] = True
return
# ============================================================================
# 主入口2:手持物品右键方块表面 → 按 itemUseOnOwner 路由到右键服务
# ============================================================================
def OnItemUseOn(self, args):
"""
手持物品右键某个方块表面时触发(如手持原版生鸡肉右键地面)。
用手持物品名定位到注册该物品的【所有】服务(itemUseOnAll,按注册顺序),
逐个尝试 handleItemUseOn,第一个返回 True 的服务处理成功:
- 食材裹粉(ShicaiService) 与 原版放置(YuanbanService) 都注册了 土豆/鸡肉/猪肉,
手持这些物品右键面粉块 → 裹粉先命中; 右键地面 → 裹粉返回 False 后交给原版放置。
取消"使用物品于方块"的字段是 ret(不是 cancel),官方要求双端设置才能
真正取消默认行为。本分发器已在服务端设 ret=True。
"""
itemName = args.get("itemName")
services = self.itemUseOnAll.get(itemName)
playerId = args.get("playerId") or args.get("entityId")
# 被点方块(用于去重: 该方块归哪些右键服务管辖)
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
# 服务会处理它, 直接取消默认放置 —— 防止"先放一个方块再消失"的体验
# (例如手持生饺子右键蒸笼、手持方块右键面团等, 意图是转换而非放置)
if clickedName and owners and (not services or not any(
s in owners for s in services)):
args['ret'] = True
return
if not services:
return
print "[FD-ItemUseOn]", itemName, "svcs:", [s.__class__.__name__ for s in services]
for service in services:
# 被点方块属于某些"右键方块"服务的管辖(条箱/挂肉钩/锅具等) → 该方块右键由
# ServerBlockUseEvent 处理(放入/挂鱼/做菜), 跳过本服务的 itemUseOn,
# 避免与方块右键逻辑重复(如手持鲑鱼右键条箱应放入而非放置死鲑鱼)。
# 例外: 当前服务本身就在被点方块的归属列表里(如 食材裹粉: 面粉块归
# ShicaiService+面食服务 管辖, 手持生食材右键它时放行给 handleItemUseOn
# 处理) 则继续尝试。
if clickedName and owners and service not in owners:
continue
handled = service.handleItemUseOn(args)
if handled:
args['ret'] = True
self._cancelItemUse[playerId] = True
return
# ============================================================================
# 主入口3:拦截物品使用(防止喝药水/吃食物等默认行为)
# ============================================================================
def OnItemTryUse(self, args):
"""
玩家尝试使用物品(喝药水、吃食物等)时触发。
如果该玩家刚被标记为"取消下一次物品使用"(_cancelItemUse),
就取消这次默认行为,避免右键转换方块时误喝药水。
"""
playerId = args.get("playerId")
print "[FD-ItemTryUse]", args.get("itemName"), "player", playerId
if self._cancelItemUse.pop(playerId, False):
args['cancel'] = True
# ============================================================================
# 主入口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 OnDestroy(self, args):
"""
玩家拆方块时触发。
注意: ServerPlayerTryDestroyBlockEvent 的方块名字段是 fullName(不是 blockName)!
优先级:
1. 有"破坏掉落"逻辑的服务(如家具的存物方块: 书柜/药水架/厨具架
被拆掉 → 掉 空方块本体+全部存储物品, 与售卖盒不同, 破坏后取不到)
2. 静态映射: 放置的死鱼/生鸡肉掉回原版生食材
"""
blockName = args.get("fullName") or args.get("blockName")
args["blockName"] = blockName # 统一成 blockName 字段, 服务内部直接用
# 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