diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/BlockSwitchService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/BlockSwitchService.py new file mode 100644 index 0000000..8ef93d3 --- /dev/null +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/BlockSwitchService.py @@ -0,0 +1,564 @@ +# -*- coding: utf-8 -*- +""" +================================================================================ +工具方块切换引擎(服务端) +================================================================================ + +这个文件负责所有"普通工具方块"的右键转换逻辑,和做菜(CookingService)彻底分开。 + +【一句话概括】 + 玩家右键工具方块(箭桶、挂肉钩、水槽、盘子架、药水架、书柜……) → + 查 BlockSwitchData 有没有匹配的规则 → 消耗物品 → 替换方块 → 保存记录 + +【为什么单独拆一个类】 + 原来做菜和工具切换都在 CookingService 里,右键时每次都要同时碰到两套数据, + 内存和逻辑都搅在一起。现在拆成两个系统: + - CookingService → 只处理做菜配方(RECIPES) + - BlockSwitchService → 只处理工具方块切换(BLOCK_SWITCHES) + 两个系统都监听同一个 ServerBlockUseEvent, + 但各自在事件第一行用 switchBlockSet/cookBlockSet(O(1) 的 set)按方块名拦截: + 不是自己的方块直接 return,互不干扰,各自只查自己那一份数据。 + +【事件监听】 + - ServerBlockUseEvent → 玩家右键方块(工具切换主入口) + - ServerItemTryUseEvent → 玩家尝试使用物品(拦截喝药水等默认行为) + +【保存/持久化】 + 箭桶、药水架、书柜这类有 record 记录的方块,把放进来的物品记录在 + 方块实体数据里(字段 rtdd_swt/rtdd_swc/rtdd_swi/rtdd_sws), + 这样玩家退出游戏再重进,loadSwitchRecord 还能从方块里把物品读回来。 +================================================================================ +""" +import time +import mod.server.extraServerApi as serverApi + +# ---- 网易API基础对象 ---- +ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类 +comp = serverApi.GetEngineCompFactory() # 组件工厂,用来创建各种功能组件 + +# ---- 继承 BlueFox 框架的系统基类 ---- +# BlueFoxServerSystem 会自动处理 LoadMod / UnloadMod 等生命周期, +# 不需要手动写,继承后框架自动发现并注册(ModLoader 的 GetLastSubclass) +from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem +from ...BlueFoxTools.ToolsConfig import LevelId + +# ---- 方块实体数据组件 ---- +# GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作,直接赋值即可持久化。 +blockEntityDataComp = comp.CreateBlockEntityData(LevelId) + +# ---- 导入工具方块切换规则 ---- +# BLOCK_SWITCHES 是一个列表,每条规则是一个 dict,格式见 BlockSwitchData.py 顶部注释 +from ..BlockSwitchData import BLOCK_SWITCHES + + +# ================================================================================ +# 工具函数:读取物品 userData 里的嵌套字段 +# ================================================================================ + +def getUserDataField(itemDict, *keys): + """ + 从物品 dict 的 userData 里一层层往下读。 + 中间某一层不存在会自动创建空 dict,不会报错。 + """ + itemDict['userData'] = itemDict.get('userData', {}) + if itemDict.get('userData') is None: + itemDict['userData'] = {} + node = itemDict['userData'] + for key in keys: + node[key] = node.get(key, {}) + node = node[key] + return node + + +# ================================================================================ +# 工具方块切换引擎主类 +# ================================================================================ + +class BlockSwitchService(BlueFoxServerSystem): + """ + 通用工具方块切换引擎 + + 【核心数据结构】 + switchRecord = { + (维度ID, x, y, z): { # 用方块坐标当 key,每个方块独立 + "count": 整数, # recordType="count" 时记录数量(如箭桶里有多少箭) + "itemName": 字符串, # recordType="count" 时记录物品名(如"minecraft:arrow") + "stack": [物品信息列表], # recordType="stack" 时记录后进先出的物品堆 + } + } + + 这个数据同时存在两处: + 1. 内存(self.switchRecord)—— 快,关服就没了 + 2. 方块实体数据(blockEntityData)—— 慢一点,但关服重启后还在 + 每次操作都会同步更新两处,保证重启不丢记录 + """ + + def __init__(self, namespace, systemName): + """ + 初始化:清空记录数据、注册事件监听、构建工具切换规则字典 + """ + super(BlockSwitchService, self).__init__(namespace, systemName) + print "===== BlockSwitchService init =====" + self.switchRecord = {} # 工具方块的记录数据(箭桶数量、药水架顺序等) + self.switchCooldown = {} # 工具方块切换冷却时间(防止连点) + self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等) + self.ListenEvent() # 注册游戏事件监听 + + # ---- 构建工具切换规则字典(O(1) 查找) ---- + # key = (方块名, 物品名),value = 规则dict + self.switchRecipes = {} + for switch in BLOCK_SWITCHES: + key = (switch["source"], switch.get("item")) + self.switchRecipes[key] = switch + + # ---- 构建工具方块名集合(O(1) 快速拦截,第一道过滤) ---- + # 事件进来先判断方块名在不在这个集合里,不在直接 return, + # 这样做菜方块的右键事件根本不会进到本系统的字典查找 + self.switchBlockSet = set() + for switch in BLOCK_SWITCHES: + self.switchBlockSet.add(switch["source"]) + + def Destroy(self): + """ + 系统销毁时取消所有事件监听,防止内存泄漏 + """ + print "===== BlockSwitchService Destroy =====" + self.UnListenEvent() + + # ============================================================================ + # 事件注册与注销 + # ============================================================================ + + def ListenEvent(self): + """ + 注册两个游戏事件: + 1. ServerBlockUseEvent —— 玩家右键方块(工具切换入口) + 2. ServerItemTryUseEvent —— 玩家尝试使用物品(拦截喝药水等默认行为) + """ + # 事件1:玩家右键方块 + self.ListenForEvent(serverApi.GetEngineNamespace(), + serverApi.GetEngineSystemName(), + "ServerBlockUseEvent", self, self.OnBlockUse) + # 事件2:玩家尝试使用物品(拦截喝药水) + self.ListenForEvent(serverApi.GetEngineNamespace(), + serverApi.GetEngineSystemName(), + "ServerItemTryUseEvent", self, self.OnItemTryUse) + + def UnListenEvent(self): + """ + 注销事件监听,和 ListenEvent 一一对应 + """ + self.UnListenForEvent(serverApi.GetEngineNamespace(), + serverApi.GetEngineSystemName(), + "ServerBlockUseEvent", self, self.OnBlockUse) + self.UnListenForEvent(serverApi.GetEngineNamespace(), + serverApi.GetEngineSystemName(), + "ServerItemTryUseEvent", self, self.OnItemTryUse) + + # ============================================================================ + # 主入口:玩家右键方块 + # ============================================================================ + + def OnBlockUse(self, args): + """ + 玩家右键方块时触发,是工具方块切换的入口。 + + 流程: + 1. 先按方块名判断是不是本系统管辖的工具方块(O(1) set 拦截) + - 不是 → 直接 return(这是做菜方块的右键,交给 CookingService 处理) + 2. 拿到被右键的方块名、玩家ID、方块坐标 + 3. 看玩家手里拿了什么物品 + 4. 拿着"方块名+手里物品名"去工具切换字典里 O(1) 查找 + 5. 命中规则 → 执行工具切换逻辑 + """ + # ---- 第一步:提取事件参数 ---- + blockName = args.get("blockName") # 被右键的方块名 + playerId = args.get("playerId") # 谁右键的 + x, y, z = args.get("x"), args.get("y"), args.get("z") # 方块坐标 + pos = (x, y, z) # 坐标打包成元组 + dim = comp.CreateDimension(playerId).GetEntityDimensionId() # 玩家在哪个维度 + key = (dim, x, y, z) # 完整的唯一标识:维度+坐标 + + # ---- 第二步:O(1) 方块名拦截 ---- + # 只有工具方块的右键才归本系统处理;做菜方块直接放过,交给 CookingService + if blockName not in self.switchBlockSet: + return + + # ---- 第三步:拿到玩家手里拿的物品 ---- + itemDict = self.getCarriedItem(playerId) + + # ---- 第四步:查工具切换规则(O(1) 字典查找) ---- + recipe = self.findRecipe(blockName, itemDict) + if not recipe: + # 方块是工具方块,但当前组合没有匹配的规则(比如手里物品不对)→ 不拦截 + return + + # 找到规则后,取消默认行为(如喝药水、吃食物等),由本 Mod 全权接管 + args['cancel'] = True + # 同时标记该玩家下一次物品使用事件也要取消,防止喝药水等默认行为 + self._cancelItemUse[playerId] = True + + # ---- 第五步:加载方块历史记录到内存 ---- + # 这样 handleSwitch 里直接从 self.switchRecord 取就行 + self._loadBlockDataToMemory(key, playerId, pos, dim) + + # ---- 第六步:执行工具切换 ---- + self.handleSwitch(recipe, playerId, pos, dim, itemDict, key, blockName) + + # ============================================================================ + # 拦截物品使用事件(防止喝药水等默认行为) + # ============================================================================ + + def OnItemTryUse(self, args): + playerId = args.get("playerId") + if self._cancelItemUse.pop(playerId, False): + args['cancel'] = True + + # ============================================================================ + # 规则匹配(O(1) 字典查找) + # ============================================================================ + + def findRecipe(self, blockName, itemDict): + """ + 在工具切换字典中 O(1) 查找匹配的规则。 + + 匹配条件: + 1. 先精确匹配 (方块名, 物品名) + 2. 如果没找到,尝试匹配 (方块名, None) —— 空手也能触发的规则 + + 返回:匹配到的规则 dict,找不到返回 None + """ + itemName = itemDict.get("newItemName") if itemDict else None + + # 精确匹配:(方块名, 物品名) + recipe = self.switchRecipes.get((blockName, itemName)) + if recipe: + return recipe + + # 空手匹配:(方块名, None) + recipe = self.switchRecipes.get((blockName, None)) + if recipe: + return recipe + + return None + + # ============================================================================ + # 工具方块切换处理 + # ============================================================================ + + def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key, blockName): + """ + 处理工具方块的切换逻辑(箭桶、挂肉钩、水槽、盘子架、药水架、书柜等)。 + + 支持三种模式: + 1. recordType="count" → 记录一种物品+数量(如箭桶存箭) + 2. recordType="stack" → 后进先出堆栈(如药水架、书柜) + 3. 无 record → 纯切换,不记录(如挂肉钩直接换状态) + + 流程: + 1. 如果是计时型(type="timer"),直接替换方块,不消耗物品 + 2. 如果是切换型(type="switch"),根据 recordType 决定记录方式 + 3. 消耗物品、替换方块、更新记录(记录写入方块实体数据,退出重进不丢) + """ + # ---- 如果规则指定了 tool,检查玩家手里是不是拿着指定工具 ---- + if recipe.get("tool"): + if not itemDict: + return + carriedItem = itemDict.get("newItemName", "") + # 简单判断:物品名是否包含工具名 + if recipe["tool"] not in carriedItem: + return + + # ---- 冷却检查:防止连点右键瞬间切换多次 ---- + now = time.time() + lastOp = self.switchCooldown.get(key, 0) + cd = recipe.get("switchCd", 0.5) + if now - lastOp < cd: + return + self.switchCooldown[key] = now + + # ---- 计时型:直接替换方块,不消耗物品,不记录 ---- + if recipe.get("type") == "timer": + if recipe.get("target"): + self.replaceBlock(pos, dim, recipe["target"], playerId) + return + + # ---- 切换型 ---- + # 内存中已有数据(_loadBlockDataToMemory 已加载),直接用 + record = self.switchRecord.get(key) + + # ---- 正向操作:往方块里放东西(source == 被右键的方块) ---- + if recipe["source"] == blockName and recipe.get("item") is not None: + # 玩家手里有物品,往方块里放 + needSave = False # 标记是否需要保存记录到方块 + if recipe.get("record"): + if recipe.get("recordType") == "count": + # 数量记录型:只记录一种物品的总数 + if not record: + record = { + "count": 0, + "itemName": itemDict["newItemName"], + } + # 检查是否满了 + maxCount = recipe.get("maxCount", 999) + if record["count"] >= maxCount: + return + # 记录物品+1 + record["count"] += 1 + needSave = True + + elif recipe.get("recordType") == "stack": + # 堆栈记录型:后进先出,记录每种物品的完整信息 + if not record: + record = {"stack": []} + maxCount = recipe.get("maxCount", 999) + if len(record["stack"]) >= maxCount: + return + # 把物品信息压入堆栈 + record["stack"].append({ + "itemName": itemDict["newItemName"], + "count": itemDict.get("count", 1), + "auxValue": itemDict.get("newAuxValue", 0), + "userData": itemDict.get("userData", {}), + }) + needSave = True + + # 消耗物品 + if recipe.get("consume", 1) > 0: + self.consumeItem(playerId, itemDict, recipe.get("consume", 1)) + + # 替换方块(旧方块销毁,新方块出现) + if recipe.get("target"): + self.replaceBlock(pos, dim, recipe["target"], playerId) + + # ★ 关键:替换完方块后再保存记录到新方块上,否则数据随旧方块一起销毁 + if needSave: + self._saveRecord(key, playerId, pos, dim, record) + + # 如果规则指定了给物品(如烧烤架浇水给桶) + if recipe.get("giveItem"): + self.giveItem(playerId, recipe["giveItem"]) + + return + + # ---- 逆向操作:从方块里取东西(空手右键或取回物品) ---- + if recipe["source"] == blockName and recipe.get("item") is None: + needSaveRecord = False # 是否需要保存记录 + needClearRecord = False # 是否需要清除记录 + + if recipe.get("record") and record: + if recipe.get("recordType") == "count": + # 数量记录型取回:给玩家对应的物品,数量-1 + if record["count"] > 0: + self.giveItem(playerId, record["itemName"]) + record["count"] -= 1 + if record["count"] <= 0: + needClearRecord = True + else: + needSaveRecord = True + + elif recipe.get("recordType") == "stack": + # 堆栈记录型取回:后进先出,取最后一个放的物品 + if record["stack"]: + topItem = record["stack"].pop() + comp.CreateItem(playerId).SpawnItemToPlayerInv( + {"itemName": topItem["itemName"], + "count": topItem["count"], + "auxValue": topItem["auxValue"], + "userData": topItem.get("userData", {})}, + playerId) + if not record["stack"]: + needClearRecord = True + else: + needSaveRecord = True + + # 如果规则直接指定了给什么物品(水槽、盘子架取回、挂肉钩退回等) + if recipe.get("giveItem"): + self.giveItem(playerId, recipe["giveItem"]) + + # 替换方块(旧方块销毁,新方块出现) + if recipe.get("target"): + self.replaceBlock(pos, dim, recipe["target"], playerId) + + # ★ 关键:替换完方块后再保存/清除记录,否则数据随旧方块一起销毁 + if needClearRecord: + self.clearSwitchRecord(key, playerId, pos, dim) + elif needSaveRecord: + self._saveRecord(key, playerId, pos, dim, record) + + return + + # ============================================================================ + # 通用持久化帮手(避免重复的 try/except/Get/Set 写法) + # ============================================================================ + + def _writeBlockData(self, dim, pos, **kwargs): + """ + 通用方块实体数据写入。 + + GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作, + 直接赋值即可持久化。官方没有 SetBlockEntityData 方法。 + """ + try: + blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) + if blockData is None: + print "[BlockSwitchService] _writeBlockData blockData is None: pos=%s dim=%s" % (pos, dim) + return + for key, value in kwargs.items(): + blockData[key] = value + except Exception as e: + print "[BlockSwitchService] _writeBlockData 写入失败: pos=%s dim=%s err=%s" % (pos, dim, str(e)) + + def _saveRecord(self, key, playerId, pos, dim, record): + """ + 同时更新内存和方块实体数据中的工具方块记录。 + + 替代原本重复出现的: + self.switchRecord[key] = record + self.saveSwitchRecord(playerId, pos, dim, record) + """ + self.switchRecord[key] = record + self.saveSwitchRecord(playerId, pos, dim, record) + + # ============================================================================ + # 统一数据加载入口(玩家右键工具方块时先把方块数据恢复到内存) + # ============================================================================ + + def _loadBlockDataToMemory(self, key, playerId, pos, dim): + """ + 在确定玩家右键命中了一个工具切换规则后,立刻把方块里存的记录加载到内存。 + + 这样 handleSwitch 里就不用自己加载了,直接从 self.switchRecord 取就行。 + """ + if key not in self.switchRecord: + switchInfo = self.loadSwitchRecord(playerId, pos, dim) + if switchInfo: + self.switchRecord[key] = switchInfo + + # ============================================================================ + # 工具方块记录持久化(药水架、书柜、箭桶等) + # ============================================================================ + + def saveSwitchRecord(self, playerId, pos, dim, record): + """ + 把工具方块的记录数据写入方块实体数据。 + 记录类型:count型(箭桶)→ rtdd_swt/swc/swi,stack型(书柜/药水架)→ rtdd_swt/sws + + 记录存在方块实体数据里,所以玩家退出游戏再重进, + loadSwitchRecord 还能从方块里把物品读回来。 + """ + if "count" in record: + self._writeBlockData(dim, pos, + rtdd_swt="count", + rtdd_swc=record["count"], + rtdd_swi=record.get("itemName", "")) + elif "stack" in record: + self._writeBlockData(dim, pos, + rtdd_swt="stack", + rtdd_sws=record["stack"]) + + def loadSwitchRecord(self, playerId, pos, dim): + """ + 从方块实体数据里读取工具方块的记录数据。 + + 返回: + 有数据 → {"count": N, "itemName": "xxx"} 或 {"stack": [...]} + 没有 → None + """ + try: + blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) + swt = blockData["rtdd_swt"] # 记录类型:"count" 或 "stack" + if swt == "count": + return { + "count": blockData["rtdd_swc"], + "itemName": blockData["rtdd_swi"], + } + elif swt == "stack": + return { + "stack": blockData["rtdd_sws"], + } + except: + return None + + def clearSwitchRecord(self, key, playerId, pos, dim): + """ + 清除工具方块的记录数据(内存+方块实体数据)。 + """ + self.switchRecord.pop(key, None) + self._writeBlockData(dim, pos, rtdd_swt="", rtdd_swc=0, rtdd_swi="", rtdd_sws=[]) + + # ============================================================================ + # 工具方法:获取玩家手中物品 + # ============================================================================ + + def getCarriedItem(self, playerId): + """ + 获取玩家主手(右手)拿的物品。 + 空手返回 None。 + """ + itemComp = comp.CreateItem(playerId) + return itemComp.GetPlayerItem( + serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0) + + # ============================================================================ + # 工具方法:消耗物品 + # ============================================================================ + + def consumeItem(self, playerId, itemDict, amount=1): + """ + 消耗玩家主手物品的指定数量。 + 如果数量不够 → 清空主手;如果数量够 → 减少对应数量。 + """ + if itemDict is None: + return + count = itemDict.get("count", 0) + if count <= amount: + # 数量不够或刚好用完 → 清空主手 + comp.CreateItem(playerId).SetEntityItem( + serverApi.GetMinecraftEnum().ItemPosType.CARRIED, + {"itemName": "minecraft:air", "count": 0, "auxValue": 0}) + else: + # 数量够 → 减少 amount 个 + itemDict["count"] = count - amount + comp.CreateItem(playerId).SetEntityItem( + serverApi.GetMinecraftEnum().ItemPosType.CARRIED, + {"itemName": itemDict["newItemName"], + "count": itemDict["count"], + "auxValue": itemDict.get("newAuxValue", 0)}) + + # ============================================================================ + # 工具方法:替换方块 + # ============================================================================ + + def replaceBlock(self, pos, dim, newBlockName, playerId, aux=None): + """ + 把指定位置的方块替换成新方块。 + + 对照 BlockBlock.py 的标准写法: + - 使用命名参数 + - oldBlockHandling=0 表示直接替换(不保留旧方块数据) + - aux 默认继承旧方块朝向,这样替换后不会转方向 + """ + if aux is None: + try: + oldBlock = comp.CreateBlockInfo(playerId).GetBlockNew(pos, dim) + aux = oldBlock.get("aux", 0) + except: + aux = 0 + comp.CreateBlockInfo(playerId).SetBlockNew( + pos, + blockDict={"name": newBlockName, "aux": aux}, + oldBlockHandling=0, + dimensionId=dim) + + # ============================================================================ + # 工具方法:给玩家物品 + # ============================================================================ + + def giveItem(self, playerId, itemName, count=1): + """ + 往玩家背包里塞一个物品。 + 如果背包满了,物品会掉在地上。 + """ + comp.CreateItem(playerId).SpawnItemToPlayerInv( + {"itemName": itemName, "count": count, "auxValue": 0}, playerId) \ No newline at end of file diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/CookingService.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/CookingService.py index 87bb08d..73a7803 100644 --- a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/CookingService.py +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/CookingService.py @@ -12,6 +12,15 @@ 玩家右键烹饪方块 → 引擎查 RecipeData 有没有匹配的配方 → 有就消耗手上物品 → 替换方块/记录翻炒进度 → 完成做菜 +【为什么和工具切换分开】 + 原本做菜和普通工具方块切换(箭桶、水槽、书架、药水架等)都在这个类里。 + 现在已拆成两个独立系统: + - CookingService → 只处理做菜配方(RECIPES),本文件 + - BlockSwitchService → 只处理工具方块切换(BLOCK_SWITCHES) + 两个系统都监听同一个 ServerBlockUseEvent, + 各自在事件第一行用 cookBlockSet/switchBlockSet(O(1) 的 set)按方块名拦截, + 不是自己的方块直接 return,互不干扰,各自只查自己那一份数据。 + 【设计原则】 - 加新菜不改代码:在 RecipeData.py 加一条配方就行,这里一行不用动 - 所有做菜逻辑一处维护:炒锅、煎锅、油锅、架锅、蒸笼……全走同一个引擎 @@ -49,11 +58,6 @@ blockEntityDataComp = comp.CreateBlockEntityData(LevelId) # RECIPES 是一个列表,每条配方是一个 dict,格式见 RecipeData.py 顶部注释 from ..RecipeData import RECIPES, inferBaseBlock, BELOW_CAMPFIRE -# ---- 导入工具方块切换规则 ---- -# BLOCK_SWITCHES 是一个列表,每条规则是一个 dict,格式见 BlockSwitchData.py 顶部注释 -from ..BlockSwitchData import BLOCK_SWITCHES - - # ================================================================================ # 工具函数:读取物品 userData 里的嵌套字段 # ================================================================================ @@ -111,14 +115,8 @@ class CookingService(BlueFoxServerSystem): 2. 方块实体数据(blockEntityData)—— 慢一点,但关服重启后还在 每次操作都会同步更新两处,保证重启不丢进度 - 【工具方块记录数据】 - switchRecord = { - (维度ID, x, y, z): { - "count": 整数, # recordType="count" 时记录数量(如箭桶里有多少箭) - "itemName": 字符串, # recordType="count" 时记录物品名(如"minecraft:arrow") - "stack": [物品信息列表], # recordType="stack" 时记录后进先出的物品堆 - } - } + 工具方块(箭桶、书架、药水架等)的记录数据已拆分到 BlockSwitchService, + 本类只负责做菜配方(RECIPES)。 """ def __init__(self, namespace, systemName): @@ -128,22 +126,24 @@ class CookingService(BlueFoxServerSystem): super(CookingService, self).__init__(namespace, systemName) print "===== CookingService init =====" self.stirData = {} # 内存中的翻炒数据字典 - self.switchRecord = {} # 工具方块的记录数据(箭桶数量、药水架顺序等) - self.switchCooldown = {} # 工具方块切换冷却时间(防止连点) self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等) self.ListenEvent() # 注册游戏事件监听 - # ---- 构建统一配方字典(O(1) 查找) ---- - # 把 RECIPES(做菜配方)和 BLOCK_SWITCHES(工具切换规则)合并成一份字典 + # ---- 构建做菜配方字典(O(1) 查找) ---- + # 只含 RECIPES(做菜配方),工具方块切换已拆到 BlockSwitchService # key = (方块名, 物品名),value = 配方dict # 这样查找时不用遍历整个列表,直接 dict[key] 就能拿到,速度飞快 self.allRecipes = {} for recipe in RECIPES: key = (recipe["source"], recipe.get("item")) self.allRecipes[key] = recipe - for switch in BLOCK_SWITCHES: - key = (switch["source"], switch.get("item")) - self.allRecipes[key] = switch + + # ---- 构建做菜方块名集合(O(1) 快速拦截,第一道过滤) ---- + # 事件进来先判断方块名在不在这个集合里,不在直接 return, + # 这样工具方块的右键事件根本不会进到本系统的字典查找 + self.cookBlockSet = set() + for recipe in RECIPES: + self.cookBlockSet.add(recipe["source"]) def Destroy(self): """ @@ -200,10 +200,11 @@ class CookingService(BlueFoxServerSystem): 流程: 1. 拿到被右键的方块名、玩家ID、方块坐标 - 2. 看玩家手里拿了什么物品 - 3. 拿着"方块名+手里物品名"去统一字典里 O(1) 查找 - 4. 找到规则后,判断是"做菜配方"还是"工具切换"类型 - 5. 执行对应的逻辑 + 2. 先按方块名判断是不是本系统管辖的做菜方块(O(1) set 拦截) + - 不是 → 直接 return(这是工具方块的右键,交给 BlockSwitchService 处理) + 3. 看玩家手里拿了什么物品 + 4. 拿着"方块名+手里物品名"去做菜字典里 O(1) 查找 + 5. 命中配方 → 查底座 → 执行做菜逻辑(翻炒/替换) args 是网易API传过来的事件参数,包含: - blockName: 被右键的方块名字(如 "rtdd:wok") @@ -218,15 +219,20 @@ class CookingService(BlueFoxServerSystem): dim = comp.CreateDimension(playerId).GetEntityDimensionId() # 玩家在哪个维度(主世界/下界/末地) key = (dim, x, y, z) # 完整的唯一标识:维度+坐标 - # ---- 第二步:拿到玩家手里拿的物品 ---- + # ---- 第二步:O(1) 方块名拦截 ---- + # 只有做菜方块的右键才归本系统处理;工具方块直接放过,交给 BlockSwitchService + if blockName not in self.cookBlockSet: + return + + # ---- 第三步:拿到玩家手里拿的物品 ---- itemDict = self.getCarriedItem(playerId) # itemDict 可能是 None(空手),也可能是 dict(手里有东西) # dict 格式:{"newItemName": "rtdd:spatula", "count": 1, "userData": {...}} - # ---- 第三步:查配方(O(1) 字典查找) ---- + # ---- 第四步:查做菜配方(O(1) 字典查找) ---- recipe = self.findRecipe(blockName, itemDict) if not recipe: - # 没找到匹配的配方 → 不是做菜操作,不拦截,让游戏正常处理 + # 方块是烹饪方块,但当前组合没有匹配的配方(比如手里物品不对)→ 不拦截 return # 找到配方后,取消默认行为(如喝药水、吃食物等),由本 Mod 全权接管 @@ -234,26 +240,15 @@ class CookingService(BlueFoxServerSystem): # 同时标记该玩家下一次物品使用事件也要取消,防止喝药水等默认行为 self._cancelItemUse[playerId] = True - # ---- 第四步:判断是不是工具切换类型 ---- - recipeType = recipe.get("type", "cook") - # type="switch" 或 "timer" → 工具方块切换(箭桶、挂肉钩、水槽等) - # 没有 type 或 type="cook" → 做菜配方(炒锅、煎锅等) - - # ---- 统一加载:把方块里的历史数据恢复到内存 ---- - # 不管是做菜还是工具切换,先把方块数据加载到内存,后面直接用 + # ---- 第五步:加载翻炒历史数据到内存 ---- + # 这样 handleStir 里直接从 self.stirData 取就行 self._loadBlockDataToMemory(key, playerId, pos, dim, recipe) - if recipeType == "switch" or recipeType == "timer": - # 工具方块切换(水槽、箭桶、挂肉钩、盘子架等)不做底座检查,直接执行 - self.handleSwitch(recipe, playerId, pos, dim, itemDict, key, blockName) - return - - # ---- 底座检查:仅做菜配方才需要,锅下面必须有灶台/营火 ---- - # 工具切换类型在上面已提前返回,不会走到这里 + # ---- 底座检查:锅下面必须有灶台/营火 ---- if not self.checkBaseBlock(recipe, playerId, pos, dim): return - # ---- 第五步:以下为做菜配方逻辑 ---- + # ---- 第六步:以下为做菜配方逻辑 ---- # 内存中已有数据(_loadBlockDataToMemory 已加载),直接用 stirInfo = self.stirData.get(key, {}) @@ -315,152 +310,6 @@ class CookingService(BlueFoxServerSystem): return None - # ============================================================================ - # 工具方块切换处理 - # ============================================================================ - - def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key, blockName): - """ - 处理工具方块的切换逻辑(箭桶、挂肉钩、水槽、盘子架、药水架、书柜等)。 - - 支持三种模式: - 1. recordType="count" → 记录一种物品+数量(如箭桶存箭) - 2. recordType="stack" → 后进先出堆栈(如药水架、书柜) - 3. 无 record → 纯切换,不记录(如挂肉钩直接换状态) - - 流程: - 1. 如果是计时型(type="timer"),直接替换方块,不消耗物品 - 2. 如果是切换型(type="switch"),根据 recordType 决定记录方式 - 3. 消耗物品、替换方块、更新记录 - """ - # ---- 如果规则指定了 tool,检查玩家手里是不是拿着指定工具 ---- - if recipe.get("tool"): - if not itemDict: - return - carriedItem = itemDict.get("newItemName", "") - # 简单判断:物品名是否包含工具名 - if recipe["tool"] not in carriedItem: - return - - # ---- 冷却检查:防止连点右键瞬间切换多次 ---- - now = time.time() - lastOp = self.switchCooldown.get(key, 0) - cd = recipe.get("switchCd", 0.5) - if now - lastOp < cd: - return - self.switchCooldown[key] = now - - # ---- 计时型:直接替换方块,不消耗物品,不记录 ---- - if recipe.get("type") == "timer": - if recipe.get("target"): - self.replaceBlock(pos, dim, recipe["target"], playerId) - return - - # ---- 切换型 ---- - # 内存中已有数据(_loadBlockDataToMemory 已加载),直接用 - record = self.switchRecord.get(key) - - # ---- 正向操作:往方块里放东西(source == 被右键的方块) ---- - if recipe["source"] == blockName and recipe.get("item") is not None: - # 玩家手里有物品,往方块里放 - needSave = False # 标记是否需要保存记录到方块 - if recipe.get("record"): - if recipe.get("recordType") == "count": - # 数量记录型:只记录一种物品的总数 - if not record: - record = { - "count": 0, - "itemName": itemDict["newItemName"], - } - # 检查是否满了 - maxCount = recipe.get("maxCount", 999) - if record["count"] >= maxCount: - return - # 记录物品+1 - record["count"] += 1 - needSave = True - - elif recipe.get("recordType") == "stack": - # 堆栈记录型:后进先出,记录每种物品的完整信息 - if not record: - record = {"stack": []} - maxCount = recipe.get("maxCount", 999) - if len(record["stack"]) >= maxCount: - return - # 把物品信息压入堆栈 - record["stack"].append({ - "itemName": itemDict["newItemName"], - "count": itemDict.get("count", 1), - "auxValue": itemDict.get("newAuxValue", 0), - "userData": itemDict.get("userData", {}), - }) - needSave = True - - # 消耗物品 - if recipe.get("consume", 1) > 0: - self.consumeItem(playerId, itemDict, recipe.get("consume", 1)) - - # 替换方块(旧方块销毁,新方块出现) - if recipe.get("target"): - self.replaceBlock(pos, dim, recipe["target"], playerId) - - # ★ 关键:替换完方块后再保存记录到新方块上,否则数据随旧方块一起销毁 - if needSave: - self._saveRecord(key, playerId, pos, dim, record) - - # 如果配方指定了给物品(如烧烤架浇水给桶) - if recipe.get("giveItem"): - self.giveItem(playerId, recipe["giveItem"]) - - return - - # ---- 逆向操作:从方块里取东西(空手右键或取回物品) ---- - if recipe["source"] == blockName and recipe.get("item") is None: - needSaveRecord = False # 是否需要保存记录 - needClearRecord = False # 是否需要清除记录 - - if recipe.get("record") and record: - if recipe.get("recordType") == "count": - # 数量记录型取回:给玩家对应的物品,数量-1 - if record["count"] > 0: - self.giveItem(playerId, record["itemName"]) - record["count"] -= 1 - if record["count"] <= 0: - needClearRecord = True - else: - needSaveRecord = True - - elif recipe.get("recordType") == "stack": - # 堆栈记录型取回:后进先出,取最后一个放的物品 - if record["stack"]: - topItem = record["stack"].pop() - comp.CreateItem(playerId).SpawnItemToPlayerInv( - {"itemName": topItem["itemName"], - "count": topItem["count"], - "auxValue": topItem["auxValue"], - "userData": topItem.get("userData", {})}, - playerId) - if not record["stack"]: - needClearRecord = True - else: - needSaveRecord = True - - # 如果配方直接指定了给什么物品(水槽、盘子架取回、挂肉钩退回等) - if recipe.get("giveItem"): - self.giveItem(playerId, recipe["giveItem"]) - - # 替换方块(旧方块销毁,新方块出现) - if recipe.get("target"): - self.replaceBlock(pos, dim, recipe["target"], playerId) - - # ★ 关键:替换完方块后再保存/清除记录,否则数据随旧方块一起销毁 - if needClearRecord: - self.clearSwitchRecord(key, playerId, pos, dim) - elif needSaveRecord: - self._saveRecord(key, playerId, pos, dim, record) - - return - # ============================================================================ # 配方执行(普通替换类型) # ============================================================================ @@ -810,38 +659,20 @@ class CookingService(BlueFoxServerSystem): except Exception as e: print "[CookingService] _writeBlockData 写入失败: pos=%s dim=%s err=%s" % (pos, dim, str(e)) - def _saveRecord(self, key, playerId, pos, dim, record): - """ - 同时更新内存和方块实体数据中的工具方块记录。 - - 替代原本重复出现的: - self.switchRecord[key] = record - self.saveSwitchRecord(playerId, pos, dim, record) - """ - self.switchRecord[key] = record - self.saveSwitchRecord(playerId, pos, dim, record) - # ============================================================================ # 统一数据加载入口(玩家右键方块时先把方块数据恢复到内存) # ============================================================================ def _loadBlockDataToMemory(self, key, playerId, pos, dim, recipe): """ - 在确定玩家右键命中了一个配方后,立刻把方块里存的历史数据加载到内存。 + 在确定玩家右键命中了一个做菜配方后,立刻把方块里存的翻炒进度加载到内存。 - 这样 handleStir / handleSwitch 里就不用各自加载了, - 直接从 self.stirData / self.switchRecord 取就行。 - - 两个数据源互不干扰:翻炒数据用 stirData,工具切换记录用 switchRecord。 + 这样 handleStir 里就不用自己加载了,直接从 self.stirData 取就行。 """ if key not in self.stirData: stirInfo = self.loadStirData(playerId, pos, dim, recipe) if stirInfo: self.stirData[key] = stirInfo - if key not in self.switchRecord: - switchInfo = self.loadSwitchRecord(playerId, pos, dim) - if switchInfo: - self.switchRecord[key] = switchInfo # ============================================================================ # 翻炒数据持久化(存方块里,不存JSON文件) @@ -894,55 +725,6 @@ class CookingService(BlueFoxServerSystem): self.stirData.pop(key, None) self._writeBlockData(dim, pos, rtdd_sc=0, rtdd_slt=0.0, rtdd_sf=False) - # ============================================================================ - # 工具方块记录持久化(药水架、书柜、箭桶等) - # ============================================================================ - - def saveSwitchRecord(self, playerId, pos, dim, record): - """ - 把工具方块的记录数据写入方块实体数据。 - 记录类型:count型(箭桶)→ rtdd_swt/swc/swi,stack型(书柜/药水架)→ rtdd_swt/sws - """ - if "count" in record: - self._writeBlockData(dim, pos, - rtdd_swt="count", - rtdd_swc=record["count"], - rtdd_swi=record.get("itemName", "")) - elif "stack" in record: - self._writeBlockData(dim, pos, - rtdd_swt="stack", - rtdd_sws=record["stack"]) - - def loadSwitchRecord(self, playerId, pos, dim): - """ - 从方块实体数据里读取工具方块的记录数据。 - - 返回: - 有数据 → {"count": N, "itemName": "xxx"} 或 {"stack": [...]} - 没有 → None - """ - try: - blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) - swt = blockData["rtdd_swt"] # 记录类型:"count" 或 "stack" - if swt == "count": - return { - "count": blockData["rtdd_swc"], - "itemName": blockData["rtdd_swi"], - } - elif swt == "stack": - return { - "stack": blockData["rtdd_sws"], - } - except: - return None - - def clearSwitchRecord(self, key, playerId, pos, dim): - """ - 清除工具方块的记录数据(内存+方块实体数据)。 - """ - self.switchRecord.pop(key, None) - self._writeBlockData(dim, pos, rtdd_swt="", rtdd_swc=0, rtdd_swi="", rtdd_sws=[]) - # ============================================================================ # 拆方块处理 # ============================================================================ diff --git a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py index 9a1a1f4..432be79 100644 --- a/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py +++ b/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/modMain.py @@ -1,5 +1,6 @@ from BlueFoxTools.ModLoader import * from ceshi.service.CookingService import CookingService +from ceshi.service.BlockSwitchService import BlockSwitchService if not serverApi.IsInServer(): pass diff --git a/behavior_pack_v8jz0zSR/netease_blocks/arrowbarrel_1.json b/behavior_pack_v8jz0zSR/netease_blocks/arrowbarrel_1.json index 9517bd6..1136a4e 100644 --- a/behavior_pack_v8jz0zSR/netease_blocks/arrowbarrel_1.json +++ b/behavior_pack_v8jz0zSR/netease_blocks/arrowbarrel_1.json @@ -6,28 +6,28 @@ "value": 0 }, "netease:aabb": { - "clip": { - "max": [ - 0.2, - 1.45, - 0.9 - ], + "collision": { "min": [ 0, 0, 0.3 + ], + "max": [ + 1, + 0.95, + 0.75 ] }, - "collision": { - "max": [ - 0.2, - 1.45, - 0.9 - ], + "clip": { "min": [ 0, 0, 0.3 + ], + "max": [ + 1, + 0.95, + 0.75 ] } },