Files
rtdd/behavior_pack_v8jz0zSR/Script_NeteaseModousIY5aSF/ceshi/service/CookingService.py
T
2026-07-27 14:59:56 +08:00

656 lines
28 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
# ---- 方块实体数据组件 ----
# 每个方块都可以存一点自定义数据(比如"翻炒了几次"),
# 这个组件就是读写这些数据的。LevelId 是全局唯一的标识符,
# 所有方块数据统一存在这个 LevelId 下,和之前的 Function.py 用同一个
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
# ---- 导入所有配方 ----
# RECIPES 是一个列表,每条配方是一个 dict,格式见 RecipeData.py 顶部注释
from ..RecipeData import RECIPES
# ================================================================================
# 工具函数:读取物品 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)—— 慢一点,但关服重启后还在
每次操作都会同步更新两处,保证重启不丢进度
"""
def __init__(self, namespace, systemName):
"""
初始化:清空翻炒数据、注册事件监听
"""
super(CookingService, self).__init__(namespace, systemName)
print "===== CookingService init ====="
self.stirData = {} # 内存中的翻炒数据字典
self.ListenEvent() # 注册游戏事件监听
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. 拿着"方块名+手里物品名"去 RecipeData 里找匹配的配方
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": {...}}
# ---- 第三步:查配方 ----
recipe = self.findRecipe(blockName, itemDict)
if not recipe:
# 没找到匹配的配方 → 不是做菜操作,不拦截,让游戏正常处理
return
# ---- 第四步:加载翻炒进度(如果这个方块之前翻过的话) ----
# 先从内存取,内存没有就从方块实体数据加载(服务器重启后恢复)
stirInfo = self.stirData.get(key, {})
if not stirInfo:
stirInfo = self.loadStirData(playerId, pos, dim, recipe)
if stirInfo:
self.stirData[key] = stirInfo # 加载成功后写入内存缓存
# ---- 第五步:根据配方类型判断 ----
# 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 findRecipe(self, blockName, itemDict):
"""
遍历 RECIPES 列表,找到第一个匹配的配方。
匹配条件:
1. 配方的 source 字段 == 被右键的方块名(比如 "rtdd:wok")
2. 如果配方指定了 item 字段,则玩家手里物品必须匹配
3. 如果配方 item 是 None,表示空手也能触发(比如点锅盖)
返回:匹配到的配方 dict,找不到返回 None
"""
# 提取玩家手里物品的名字(空手时 itemName 为 None)
itemName = itemDict.get("newItemName") if itemDict else None
for recipe in RECIPES:
# 条件1:方块名必须匹配
if recipe["source"] != blockName:
continue
# 条件2:物品名必须匹配(None 表示不需要特定物品)
if recipe["item"] is not None and recipe["item"] != itemName:
continue
# 两个条件都满足 → 找到了
return recipe
# 遍历完所有配方都没找到
return None
# ============================================================================
# 配方执行(普通替换类型)
# ============================================================================
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. 从内存或方块数据加载当前翻炒进度
2. 检查冷却时间,没冷却好就忽略
3. 翻炒次数+1,更新进度
4. 给玩家发进度提示("翻炒中... 3/10")
5. 翻够了 → 标记 finished=True,或者直接替换方块
"""
now = time.time()
# ---- 第一步:加载或初始化翻炒数据 ----
data = self.stirData.get(key) or self.loadStirData(playerId, pos, dim, recipe)
if not data:
# 全新的翻炒,初始化数据
data = {
"count": 0, # 从0次开始
"lastTime": 0.0, # 上次翻的时间(初始为0,第一次翻不受冷却限制)
"finished": False, # 还没熟
"dropIfDone": recipe.get("dropIfDone", []), # 熟了拆掉掉什么
"dropIfNot": recipe.get("dropIfNot", []), # 没熟拆掉掉什么
}
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):
"""
把指定位置的方块替换成新方块。
参数:
pos: (x, y, z) 元组
dim: 维度ID
newBlockName: 新方块的完整ID,如 "rtdd:oilpan"
playerId: 玩家ID(用于创建组件)
对照 BlockBlock.py 的标准写法:
- 使用命名参数
- oldBlockHandling=0 表示直接替换(不保留旧方块数据)
- aux=0 表示默认附加值
"""
comp.CreateBlockInfo(playerId).SetBlockNew(
pos,
blockDict={"name": newBlockName, "aux": 0},
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
# ============================================================================
# 翻炒数据持久化(存方块里,不存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):
"""
把翻炒进度写入方块的实体数据。
每次翻炒后都调用一次,保证数据实时同步。
用方括号 [] 直接赋值,网易API会自动保存到方块中。
"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
blockData["rtdd_sc"] = data["count"] # 保存翻炒次数
blockData["rtdd_slt"] = data["lastTime"] # 保存最后翻炒时间
blockData["rtdd_sf"] = data["finished"] # 保存是否熟了
except:
# 如果方块没有实体数据对象,忽略(下次加载会重新初始化)
pass
def clearStirData(self, key, playerId, pos, dim):
"""
清除方块上的翻炒数据。
调用时机:
- 方块被替换(比如炒锅变成了成品菜)
- 方块被拆掉(玩家把锅拆了)
同时清除内存和方块实体数据。
"""
# 清除内存中的数据
self.stirData.pop(key, None)
# 清除方块实体数据(重置为初始值)
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
blockData["rtdd_sc"] = 0
blockData["rtdd_slt"] = 0.0
blockData["rtdd_sf"] = False
except:
pass
# ============================================================================
# 拆方块处理
# ============================================================================
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)