Files
rtdd/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/CookingService.py
T

991 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 返回的 BlockEntityData 对象支持 [] 操作,直接赋值即可持久化。
# 参考官方文档:方块实体数据组件只有 GetBlockEntityData,没有 SetBlockEntityData。
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 里的嵌套字段
# ================================================================================
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.switchCooldown = {} # 工具方块切换冷却时间(防止连点)
self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等)
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 —— 玩家拆方块(掉落食材)
3. ServerItemTryUseEvent —— 玩家尝试使用物品(拦截喝药水等默认行为)
"""
# 事件1:玩家右键方块
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerBlockUseEvent", self, self.OnBlockUse)
# 事件2:玩家尝试拆方块
self.ListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerPlayerTryDestroyBlockEvent", self, self.OnDestroy)
# 事件3:玩家尝试使用物品(拦截喝药水)
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(),
"ServerPlayerTryDestroyBlockEvent", self, self.OnDestroy)
self.UnListenForEvent(serverApi.GetEngineNamespace(),
serverApi.GetEngineSystemName(),
"ServerItemTryUseEvent", self, self.OnItemTryUse)
# ============================================================================
# 主入口:玩家右键方块
# ============================================================================
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
# 找到配方后,取消默认行为(如喝药水、吃食物等),由本 Mod 全权接管
args['cancel'] = True
# 同时标记该玩家下一次物品使用事件也要取消,防止喝药水等默认行为
self._cancelItemUse[playerId] = True
# ---- 第四步:判断是不是工具切换类型 ----
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
# ---- 底座检查:仅做菜配方才需要,锅下面必须有灶台/营火 ----
# 工具切换类型在上面已提前返回,不会走到这里
if not self.checkBaseBlock(recipe, playerId, pos, dim):
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)
# ============================================================================
# 拦截物品使用事件(防止喝药水等默认行为)
# ============================================================================
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 为 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
# ---- 冷却检查:防止连点右键瞬间切换多次 ----
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
# ============================================================================
# 配方执行(普通替换类型)
# ============================================================================
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 checkBaseBlock(self, recipe, playerId, pos, dim):
"""
检查锅下面有没有合法的底座(灶台或营火)。
优先级:
1. 配方里明确写了 "below" 字段 → 用配方的值
- below=[] 表示不需要底座,直接通过
- below=["xxx"] 表示必须是这些方块之一
2. 配方没写 "below" → 调用 inferBaseBlock 自动推断
- 大多数烹饪方块默认需要灶台(BELOW_STOVE)
- 架锅系列需要营火(BELOW_CAMPFIRE)
- 手工工具(榨汁机、搅拌器等)不需要底座
返回 True=底座合法/不需要底座,False=底座不合法
"""
# 配方显式指定了 below
if "below" in recipe:
required = recipe["below"]
if not required: # 空列表 = 不需要底座
return True
else:
# 自动推断
required = inferBaseBlock(recipe["source"])
if not required: # 推断结果为空 = 不需要底座
return True
# 检查下面一格方块
belowPos = (pos[0], pos[1] - 1, pos[2])
try:
belowBlock = comp.CreateBlockInfo(playerId).GetBlockNew(belowPos, dim)
if belowBlock.get("name") in required:
return True
except:
pass
# ---- 底座不通过,给玩家提示 ----
if required == BELOW_CAMPFIRE:
tip = "§c架锅下面需要放一个营火,才能做饭!"
else:
tip = "§c锅下面需要放一个灶台(点燃的炉灶),才能做饭!"
comp.CreateMsg(playerId).NotifyOneMessage(playerId, tip)
return False
# ============================================================================
# 条件检查
# ============================================================================
def checkConditions(self, recipe, playerId, pos, dim, itemDict):
"""
检查配方的前置条件是否满足。
目前只检查 needOil:玩家手里的物品必须带有油标签。
底座检查已移到入口 checkBaseBlock 统一处理。
"""
# ---- 检查物品是否有油标签 ----
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):
"""
消耗玩家主手物品的指定数量。
如果数量不够 → 清空主手
如果数量够 → 减少对应数量
重要:SetInvItemNum 的参数是背包槽位索引(int),不是 ItemPosType 枚举。
"""
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):
"""
把指定位置的方块替换成新方块。
参数:
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):
"""
通用方块实体数据写入。
GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作,
直接赋值即可持久化。官方没有 SetBlockEntityData 方法。
"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
print "[CookingService] _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 "[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)