# -*- 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