# -*- coding: utf-8 -*- """ ================================================================================ 通用烹饪引擎(服务端) ================================================================================ 这个文件是整个模组最核心的"大脑",所有做菜相关的逻辑都在这里。 它不关心具体做什么菜——具体菜谱全在 RecipeData.py 里。 它只负责"当玩家对某个方块右键时,查菜谱、耗材料、换方块、记进度"这套通用流程。 【一句话概括】 玩家右键烹饪方块 → 引擎查 RecipeData 有没有匹配的配方 → 有就消耗手上物品 → 替换方块/记录翻炒进度 → 完成做菜 【设计原则】 - 加新菜不改代码:在 RecipeData.py 加一条配方就行,这里一行不用动 - 所有做菜逻辑一处维护:炒锅、煎锅、油锅、架锅、蒸笼……全走同一个引擎 - 翻炒进度存方块里:服务器重启后数据不丢,玩家能继续翻 【用法】 在 modMain.py 里注册这个系统即可: serverApi.RegisterSystem("rtdd", "CookingService", "Script_NeteaseModousIY5aSF.ceshi.service.CookingService.CookingService") 【事件监听】 - ServerBlockUseEvent → 玩家右键方块(做菜主入口) - ServerPlayerTryDestroyBlockEvent → 玩家拆方块(掉落食材) ================================================================================ """ import time import mod.server.extraServerApi as serverApi # ---- 网易API基础对象 ---- ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类 comp = serverApi.GetEngineCompFactory() # 组件工厂,用来创建各种功能组件 # ---- 继承 BlueFox 框架的系统基类 ---- # BlueFoxServerSystem 会自动处理 LoadMod / UnloadMod 等生命周期, # 不需要手动写,继承后框架自动发现并注册 from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem from ...BlueFoxTools.ToolsConfig import LevelId # ---- 方块实体数据组件 ---- # 每个方块都可以存一点自定义数据(比如"翻炒了几次")。 # GetBlockEntityData / SetBlockEntityData 都在 BlockInfo 组件上。 # 注意:CreateBlockEntityData 返回的组件只有 GetBlockEntityData(只读), # 没有 SetBlockEntityData,所以必须用 CreateBlockInfo。 blockEntityDataComp = comp.CreateBlockInfo(LevelId) # ---- 导入所有配方 ---- # RECIPES 是一个列表,每条配方是一个 dict,格式见 RecipeData.py 顶部注释 from ..RecipeData import RECIPES # ---- 导入工具方块切换规则 ---- # BLOCK_SWITCHES 是一个列表,每条规则是一个 dict,格式见 BlockSwitchData.py 顶部注释 from ..BlockSwitchData import BLOCK_SWITCHES # ================================================================================ # 工具函数:读取物品 userData 里的嵌套字段 # ================================================================================ def getUserDataField(itemDict, *keys): """ 从物品 dict 的 userData 里一层层往下读。 我的世界物品可以携带隐藏数据(userData),比如一个锅铲可以标记"有油"。 这个函数就是方便地读取这些嵌套数据,不用每次都写一堆 .get()。 例子: itemDict = { "newItemName": "rtdd:spatula", "userData": {"rtdd": {"oil": True}} } getUserDataField(itemDict, 'rtdd', 'oil') → 返回 True 如果中间某一层不存在,会自动创建空 dict,不会报错。 """ # 确保 userData 字段存在 itemDict['userData'] = itemDict.get('userData', {}) if itemDict.get('userData') is None: itemDict['userData'] = {} # 一层层往下钻,路径上不存在的 key 自动创建空 dict node = itemDict['userData'] for key in keys: node[key] = node.get(key, {}) node = node[key] return node # ================================================================================ # 烹饪引擎主类 # ================================================================================ class CookingService(BlueFoxServerSystem): """ 通用烹饪引擎 【核心数据结构】 stirData = { (维度ID, x, y, z): { # 用方块坐标当 key,每个方块独立 "count": 整数, # 已经翻炒了多少次(比如翻了3次) "lastTime": 浮点数, # 上次翻炒的时间戳(用于冷却判断) "finished": 布尔值, # 是否已经熟了(熟了才能出锅) "dropIfDone": [物品名列表], # 熟了拆掉方块时掉什么 "dropIfNot": [物品名列表], # 没熟拆掉方块时掉什么 } } 这个数据同时存在两处: 1. 内存(self.stirData)—— 快,关服就没了 2. 方块实体数据(blockEntityData)—— 慢一点,但关服重启后还在 每次操作都会同步更新两处,保证重启不丢进度 【工具方块记录数据】 switchRecord = { (维度ID, x, y, z): { "count": 整数, # recordType="count" 时记录数量(如箭桶里有多少箭) "itemName": 字符串, # recordType="count" 时记录物品名(如"minecraft:arrow") "stack": [物品信息列表], # recordType="stack" 时记录后进先出的物品堆 } } """ def __init__(self, namespace, systemName): """ 初始化:清空翻炒数据、注册事件监听、构建统一配方字典 """ super(CookingService, self).__init__(namespace, systemName) print "===== CookingService init =====" self.stirData = {} # 内存中的翻炒数据字典 self.switchRecord = {} # 工具方块的记录数据(箭桶数量、药水架顺序等) self.ListenEvent() # 注册游戏事件监听 # ---- 构建统一配方字典(O(1) 查找) ---- # 把 RECIPES(做菜配方)和 BLOCK_SWITCHES(工具切换规则)合并成一份字典 # 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 def Destroy(self): """ 系统销毁时取消所有事件监听,防止内存泄漏 """ print "===== CookingService Destroy =====" self.UnListenEvent() # ============================================================================ # 事件注册与注销 # ============================================================================ def ListenEvent(self): """ 注册两个游戏事件: 1. ServerBlockUseEvent —— 玩家右键方块(做菜、翻炒、加油等) 2. ServerPlayerTryDestroyBlockEvent —— 玩家拆方块(拆锅时掉落食材) """ # 事件1:玩家右键方块 self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerBlockUseEvent", self, self.OnBlockUse) # 事件2:玩家尝试拆方块 self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerPlayerTryDestroyBlockEvent", self, self.OnDestroy) def UnListenEvent(self): """ 注销事件监听,和 ListenEvent 一一对应 """ self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerBlockUseEvent", self, self.OnBlockUse) self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "ServerPlayerTryDestroyBlockEvent", self, self.OnDestroy) # ============================================================================ # 主入口:玩家右键方块 # ============================================================================ def OnBlockUse(self, args): """ 玩家右键方块时触发,是整个做菜流程的入口。 流程: 1. 拿到被右键的方块名、玩家ID、方块坐标 2. 看玩家手里拿了什么物品 3. 拿着"方块名+手里物品名"去统一字典里 O(1) 查找 4. 找到规则后,判断是"做菜配方"还是"工具切换"类型 5. 执行对应的逻辑 args 是网易API传过来的事件参数,包含: - blockName: 被右键的方块名字(如 "rtdd:wok") - playerId: 右键的玩家ID - x, y, z: 方块坐标 """ # ---- 第一步:提取事件参数 ---- 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) # 完整的唯一标识:维度+坐标 # ---- 第二步:拿到玩家手里拿的物品 ---- itemDict = self.getCarriedItem(playerId) # itemDict 可能是 None(空手),也可能是 dict(手里有东西) # dict 格式:{"newItemName": "rtdd:spatula", "count": 1, "userData": {...}} # ---- 第三步:查配方(O(1) 字典查找) ---- recipe = self.findRecipe(blockName, itemDict) if not recipe: # 没找到匹配的配方 → 不是做菜操作,不拦截,让游戏正常处理 return # ---- 第四步:判断是不是工具切换类型 ---- recipeType = recipe.get("type", "cook") # type="switch" 或 "timer" → 工具方块切换(箭桶、挂肉钩、水槽等) # 没有 type 或 type="cook" → 做菜配方(炒锅、煎锅等) # ---- 统一加载:把方块里的历史数据恢复到内存 ---- # 不管是做菜还是工具切换,先把方块数据加载到内存,后面直接用 self._loadBlockDataToMemory(key, playerId, pos, dim, recipe) if recipeType == "switch" or recipeType == "timer": self.handleSwitch(recipe, playerId, pos, dim, itemDict, key, blockName) return # ---- 第五步:以下为做菜配方逻辑 ---- # 内存中已有数据(_loadBlockDataToMemory 已加载),直接用 stirInfo = self.stirData.get(key, {}) # ---- 根据配方类型判断 ---- # needFinished=True 的配方 → 需要先翻炒熟了才能操作(比如出锅) # needFinished=False 的配方 → 只能对没熟的进行操作(比如继续加油) if recipe.get("needFinished") is True and not stirInfo.get("finished"): # 配方要求"已熟",但当前状态是"没熟" → 不执行 return if recipe.get("needFinished") is False and stirInfo.get("finished"): # 配方要求"没熟",但当前状态是"已熟" → 不执行 return # ---- 分发到不同的处理逻辑 ---- if recipe.get("stirCount"): # 这个配方有 stirCount 字段 → 需要翻炒,走翻炒流程 self.handleStir(key, recipe, playerId, pos, dim, itemDict) return # 没有 stirCount → 普通替换流程(比如加油、加水、盖盖) self.executeRecipe(recipe, playerId, pos, dim, itemDict, key) # ============================================================================ # 配方匹配(O(1) 字典查找) # ============================================================================ def findRecipe(self, blockName, itemDict): """ 在统一字典中 O(1) 查找匹配的配方。 匹配条件: 1. 先精确匹配 (方块名, 物品名) 2. 如果没找到,尝试匹配 (方块名, None) —— 空手也能触发的规则 返回:匹配到的配方 dict,找不到返回 None """ # 提取玩家手里物品的名字(空手时 itemName 为 None) itemName = itemDict.get("newItemName") if itemDict else None # 精确匹配:(方块名, 物品名) recipe = self.allRecipes.get((blockName, itemName)) if recipe: return recipe # 空手匹配:(方块名, None) recipe = self.allRecipes.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 # ---- 计时型:直接替换方块,不消耗物品,不记录 ---- 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 # ============================================================================ # 配方执行(普通替换类型) # ============================================================================ def executeRecipe(self, recipe, playerId, pos, dim, itemDict, key): """ 执行一个"普通替换"类型的配方。 这种配方不需要翻炒,直接消耗物品、替换方块、给奖励。 典型场景: - 空炒锅 + 锅铲 → 倒了油的炒锅(加油) - 倒了油的炒锅 + 食材 → 正在炒的炒锅(下菜) - 空架锅 + 水桶 → 加了水的架锅(加水) 流程: 1. 检查前置条件(下面有没有底座、物品有没有油标签等) 2. 消耗玩家手里的物品 3. 如果需要去油,去掉物品上的油标签 4. 替换方块(比如从"空炒锅"变成"倒了油的炒锅") 5. 如果需要给玩家物品,塞进背包 """ # ---- 第一步:检查前置条件 ---- if not self.checkConditions(recipe, playerId, pos, dim, itemDict): # 条件不满足(比如下面没有灶台),什么都不做 return # ---- 第二步:消耗物品 ---- if recipe.get("consume", 1) > 0: # consume 默认是 1,表示消耗 1 个物品 # 如果 consume=0 表示不消耗(比如锅盖只是点一下,不消耗锅盖本身) self.consumeItem(playerId, itemDict, recipe.get("consume", 1)) # ---- 第三步:如果有 needOil 标记,去掉物品上的油标签 ---- if recipe.get("needOil"): self.removeOil(itemDict, playerId) # ---- 第四步:替换方块 ---- if recipe.get("target"): # target 是目标方块名,比如 "rtdd:oilpan"(倒了油的煎锅) self.replaceBlock(pos, dim, recipe["target"], playerId) # 方块变了,旧方块的翻炒数据也要清除 self.clearStirData(key, playerId, pos, dim) # ---- 第五步:给玩家奖励物品 ---- if recipe.get("giveItem"): self.giveItem(playerId, recipe["giveItem"]) # ============================================================================ # 翻炒处理 # ============================================================================ def handleStir(self, key, recipe, playerId, pos, dim, itemDict): """ 处理需要"翻炒"的配方。 翻炒类配方的特点: - 玩家右键一次 = 翻一次 - 有冷却时间(stirCd),防止连点器秒熟 - 翻够次数(stirCount)后才算"熟" - 熟了之后可以继续右键"出锅"(变成成品菜) 流程: 1. 从内存取翻炒进度(_loadBlockDataToMemory 已提前加载) 2. 检查冷却时间,没冷却好就忽略 3. 翻炒次数+1,更新进度 4. 给玩家发进度提示("翻炒中... 3/10") 5. 翻够了 → 标记 finished=True,或者直接替换方块 """ now = time.time() # ---- 第一步:从内存取翻炒数据(入口已通过 _loadBlockDataToMemory 加载) ---- data = self.stirData.get(key) if not data: # 全新的翻炒,初始化数据 data = { "count": 0, "lastTime": 0.0, "finished": False, "dropIfDone": recipe.get("dropIfDone", []), "dropIfNot": recipe.get("dropIfNot", []), } else: # 兜底防止 None 值(方块数据可能不完整) for field, default in [("lastTime", 0.0), ("count", 0), ("finished", False)]: if data.get(field) is None: data[field] = default self.stirData[key] = data # ---- 第二步:冷却检查 ---- # 防止玩家狂点右键,一秒翻100次直接熟了 # stirCd 默认 1.0 秒,即两次翻炒之间至少间隔 1 秒 if now - data["lastTime"] < recipe.get("stirCd", 1.0): # 还在冷却中,这次点击被忽略 return # ---- 第三步:翻炒次数+1 ---- data["count"] += 1 data["lastTime"] = now self.stirData[key] = data # 同步保存到方块实体数据,防止服务器重启丢失 self.saveStirData(playerId, pos, dim, data) # ---- 第四步:给玩家发进度提示 ---- comp.CreateMsg(playerId).NotifyOneMessage( playerId, "翻炒中... %s/%s" % (data["count"], recipe["stirCount"])) # 玩家会看到 "翻炒中... 3/10" 这样的提示 # ---- 第五步:判断是否翻够了 ---- if data["count"] >= recipe["stirCount"]: # 翻够了!根据 onDone 字段决定后续行为 if recipe.get("onDone") == "finish": # 标记为"熟了",方块不变,玩家可以继续右键出锅 data["finished"] = True self.stirData[key] = data self.saveStirData(playerId, pos, dim, data) if recipe.get("doneMsg"): # 发完成提示,比如 "菜炒好了!" comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["doneMsg"]) elif recipe.get("target"): # 有 target 字段 → 直接替换方块(比如翻炒到一定程度自动变成下一个状态) self.replaceBlock(pos, dim, recipe["target"], playerId) self.clearStirData(key, playerId, pos, dim) # ============================================================================ # 条件检查 # ============================================================================ def checkConditions(self, recipe, playerId, pos, dim, itemDict): """ 检查配方的前置条件是否满足。 目前支持两种条件: 1. below:锅下面一格必须是特定方块(比如灶台或营火) 2. needOil:玩家手里的物品必须带有油标签 两个条件都满足才返回 True,有一个不满足就返回 False。 """ # ---- 条件1:检查下面是否有指定方块(底座检查) ---- if recipe.get("below"): # 下面一格坐标:(x, y-1, z) belowPos = (pos[0], pos[1] - 1, pos[2]) blockComp = comp.CreateBlockInfo(playerId) try: belowBlock = blockComp.GetBlockNew(belowPos, dim) # 比较下面方块的名字是否和配方要求的底座一致 if belowBlock.get("name") != recipe["below"]: return False except: # 读取失败(比如方块不存在)→ 条件不满足 return False # ---- 条件2:检查物品是否有油标签 ---- if recipe.get("needOil") and itemDict: if not self.getItemOil(itemDict): return False # 所有条件都通过了 return True # ============================================================================ # 工具方法:获取玩家手中物品 # ============================================================================ def getCarriedItem(self, playerId): """ 获取玩家主手(右手)拿的物品。 返回格式: {"newItemName": "rtdd:spatula", "count": 1, "newAuxValue": 0, "userData": {...}} 空手返回 None。 注意区分: - CARRIED = 主手(右手) - OFFHAND = 副手(左手) - 这里只取主手 """ itemComp = comp.CreateItem(playerId) return itemComp.GetPlayerItem( serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0) # ============================================================================ # 工具方法:消耗物品 # ============================================================================ def consumeItem(self, playerId, itemDict, amount=1): """ 消耗玩家手里物品的指定数量。 如果数量不够 → 直接清空这个格子 如果数量够 → 减少对应数量 重要:网易API里 GetPlayerItem 返回的 key 是 newItemName/newAuxValue, 但 SetEntityItem 接收的 key 是 itemName/auxValue, 所以这里需要做字段名转换。 """ count = itemDict.get("count", 0) if count <= amount: # 数量不够或刚好用完 → 清空这个格子 comp.CreateItem(playerId).SetInvItemNum( serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0) else: # 数量够 → 减少 amount 个 itemDict["count"] = count - amount comp.CreateItem(playerId).SetEntityItem( serverApi.GetMinecraftEnum().ItemPosType.CARRIED, {"itemName": itemDict["newItemName"], # 注意:这里是 itemName 不是 newItemName "count": itemDict["count"], "auxValue": itemDict.get("newAuxValue", 0)}) # ============================================================================ # 工具方法:替换方块 # ============================================================================ def replaceBlock(self, pos, dim, newBlockName, playerId, aux=None): """ 把指定位置的方块替换成新方块。 参数: pos: (x, y, z) 元组 dim: 维度ID newBlockName: 新方块的完整ID,如 "rtdd:oilpan" playerId: 玩家ID(用于创建组件) aux: 方块附加值(朝向)。None=自动继承旧方块的朝向,数字=强制指定 对照 BlockBlock.py 的标准写法: - 使用命名参数 - oldBlockHandling=0 表示直接替换(不保留旧方块数据) - aux 默认继承旧方块朝向,这样替换后不会转方向 """ # 如果没指定 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) # ============================================================================ # 油标签操作 # ============================================================================ def getItemOil(self, itemDict): """ 读取物品上的油标签。 物品的 userData 里有一个 rtdd.oil 字段, 为 True 表示"这个锅铲上有油",可以在锅里倒油。 这是纯本地 dict 操作,不调网易API,非常快。 """ try: return getUserDataField(itemDict, 'rtdd').get('oil', False) except: return False def removeOil(self, itemDict, playerId): """ 去掉物品上的油标签,并同步回玩家手里的物品。 流程: 1. 把本地 dict 里的 oil 标记改为 False 2. 调用 SetEntityItem 把修改后的 userData 写回玩家手里的物品 注意:SetEntityItem 会覆盖整个物品数据, 所以必须把 itemName、count、auxValue 都原样传回去。 """ try: # 本地修改 oil 标记 getUserDataField(itemDict, 'rtdd')['oil'] = False # 同步回玩家手里的物品 itemComp = comp.CreateItem(playerId) itemComp.SetEntityItem( serverApi.GetMinecraftEnum().ItemPosType.CARRIED, {"itemName": itemDict["newItemName"], "count": itemDict.get("count", 1), "auxValue": itemDict.get("newAuxValue", 0), "userData": itemDict.get("userData", {})}) except: pass # ============================================================================ # 通用持久化帮手(避免重复的 try/except/Get/Set 写法) # ============================================================================ def _writeBlockData(self, dim, pos, **kwargs): """ 通用方块实体数据写入:读 → 改 → 写。 把 saveStirData / clearStirData / saveSwitchRecord / clearSwitchRecord 四个方法里重复的 try/except/GetBlockEntityData/SetBlockEntityData 抽出来, 一行搞定,不用每次写一样的 try/except。 """ try: blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) if blockData is None: blockData = {} for key, value in kwargs.items(): blockData[key] = value blockEntityDataComp.SetBlockEntityData(dim, pos, blockData) 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。 """ 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文件) # ============================================================================ def loadStirData(self, playerId, pos, dim, recipe): """ 从方块的实体数据里读取翻炒进度。 为什么要把翻炒进度存在方块里? → 因为服务器重启后内存就没了,存在方块实体数据里可以持久化。 → 玩家重启服务器后,之前炒了一半的菜还能继续炒。 存储的三个字段(存在方块实体数据中): rtdd_sc = stir count → 翻炒次数 rtdd_slt = stir last time → 上次翻炒时间戳 rtdd_sf = stir finished → 是否已熟 重要:GetBlockEntityData 返回的是 BlockEntityData 对象,不是普通 dict! 必须用方括号 [] 取值,不能用 .get(),否则会抛异常。 """ try: blockData = blockEntityDataComp.GetBlockEntityData(dim, pos) stirCount = blockData["rtdd_sc"] # 翻炒次数 return { "count": stirCount, "lastTime": blockData["rtdd_slt"], # 上次翻炒时间 "finished": blockData["rtdd_sf"], # 是否熟了 # dropIfDone 和 dropIfNot 从配方里取,不存方块数据中 "dropIfDone": recipe.get("dropIfDone", []), "dropIfNot": recipe.get("dropIfNot", []), } except: # 方块可能没有实体数据(从没被翻炒过),返回空 dict return {} def saveStirData(self, playerId, pos, dim, data): """ 把翻炒进度写入方块实体数据。 """ self._writeBlockData(dim, pos, rtdd_sc=data["count"], rtdd_slt=data["lastTime"], rtdd_sf=data["finished"]) def clearStirData(self, key, playerId, pos, dim): """ 清除方块上的翻炒数据(内存+方块实体数据)。 """ 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=[]) # ============================================================================ # 拆方块处理 # ============================================================================ def OnDestroy(self, args): """ 玩家拆方块时触发。 如果这个方块有翻炒数据(说明之前有人在炒菜): - 熟了(finished=True)→ 掉落"熟的东西"(dropIfDone) - 没熟(finished=False)→ 掉落"生的东西"(dropIfNot) 这样玩家拆锅时不会血本无归——炒了一半的菜至少能拿回食材。 args 参数: - playerId: 拆方块的玩家 - x, y, z: 被拆的方块坐标 """ playerId = args.get("playerId") x, y, z = args.get("x"), args.get("y"), args.get("z") dim = comp.CreateDimension(playerId).GetEntityDimensionId() key = (dim, x, y, z) # ---- 先尝试从内存取翻炒数据 ---- data = self.stirData.get(key) if not data: # 内存没有 → 尝试从方块实体数据加载(服务器重启过的情况) data = self.loadStirData(playerId, (x, y, z), dim, {}) if not data: # 完全没有翻炒数据 → 不是烹饪方块,正常拆 return # ---- 根据是否熟了决定掉落什么 ---- if data.get("finished"): dropItems = data.get("dropIfDone", []) # 熟了 → 掉熟的东西 else: dropItems = data.get("dropIfNot", []) # 没熟 → 掉生的东西 # ---- 逐个生成掉落物 ---- for itemName in dropItems: itemDict = {"itemName": itemName, "count": 1, "auxValue": 0} # 掉落物生成在方块上方 0.5 格的位置,让它自然掉到地上 self.CreateEngineItemEntity(itemDict, dim, (x, y + 0.5, z)) # ---- 清除数据 ---- self.clearStirData(key, playerId, (x, y, z), dim)