# -*- 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 # ---- 汤锅盛出自定义事件名(与客户端 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 逐个尝试 # ---- 拆方块掉落映射: 方块名 → 掉落对应的原版生食材 ---- # 玩家左键破坏这些"放置的半成品/死鱼"方块时, 取消默认掉落, 改为掉对应的原版生食材 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) 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(), "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(), "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() 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() 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)) 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: 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: 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: 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 # ============================================================================ # 主入口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