365 lines
19 KiB
Python
365 lines
19 KiB
Python
# -*- 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
|