991 lines
43 KiB
Python
991 lines
43 KiB
Python
# -*- 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) |