烧烤 动物养殖 分类 转换功能 计时转换 退出重新判断tick 解耦

This commit is contained in:
wenhe committed 2026-09-02 01:08:43 +08:00
1 parent f359008ac3
commit 991239bc26
4 files changed
+264 -432

No files matched your search

@@ -1,25 +1,39 @@
# -*- coding: utf-8 -*-
"""
================================================================================
烧烤引擎(服务端)
烧烤·右键转换引擎(服务端)
================================================================================
只处理"烧烤"(xy_barbecue) 分类下的方块转换:烧烤架、烤串、烤鸡、烤乳猪、烤鱼。
只处理"烧烤"(xy_barbecue) 分类下的【右键切换型】转换:
烧烤架点火/灭火、烤串翻烤2次、上杆(手持生食材右键支杆)、取下成品。
【与 BlockSwitchService / CookingService 的区别】
本系统(方案A)采用"统一分发器"架构:
- FeatureDispatcher 是唯一监听 ServerBlockUseEvent 的系统
- 本类通过 FeatureDispatcher.register(self) 把管辖方块注册进去
- 玩家右键方块时,FeatureDispatcher 用 O(1) dict 直接找到本实例
- 本类只查自己的 BarbecueData(BARBECUE_SWITCHES),不碰其他功能数据库
【与计时转换解耦】
同分类的"计时自动转换"(烤鸡/烤乳猪/烤鱼 上杆后下方篝火烤熟)已拆到独立类:
BarbecueTimerService —— 处理 ServerBlockEntityTickEvent 转发的计时逻辑
本类只负责右键事件,两者通过 FeatureDispatcher 协作:
- 本类右键碰到"计时源方块"(如烤鸡(生))→ 调 FeatureDispatcher.startTimerFor
转发给 BarbecueTimerService 启动烤制(篝火检查也在计时服务里做)
- 计时到点转换完全由 BarbecueTimerService 负责,本类不碰任何计时数据
【本类额外处理的规则】
- 手持"原版生鸡肉"右键方块表面(ServerItemUseOnEvent) → 生成"生鸡肉(放置后)"方块
该规则通过 FeatureDispatcher 的 itemUseOn 路由转发到本类 handleItemUseOn
【事件监听在哪?】
本类【不】监听任何事件!
所有事件由 FeatureDispatcher(xiangyemod/service/FeatureDispatcher.py)统一监听:
- ServerBlockUseEvent → OnBlockUse → 按 blockOwner 路由到本类 handleBlockUse
- ServerItemUseOnEvent → OnItemUseOn → 按 itemUseOnOwner 路由到本类 handleItemUseOn
- ServerBlockEntityTickEvent → OnBlockEntityTick → 按 timerOwner 路由到 BarbecueTimerService
本类只实现被分发器调用的接口方法,不关心事件来源。
【转换类型】
- type="switch" : 右键直接转换(点火/灭火/烤串2次/上杆/取下)
- type="timer" : 计时自动转换(上杆后下方篝火烤熟),
由本类用 AddTimer 实现,启动前检查下方是否为篝火
【本类怎么做(自上而下的方法系列)】
handleBlockUse ← 入口1:分发器转发右键方块事件
handleItemUseOn ← 入口2:分发器转发"手持物品右键方块表面"事件(放置生鸡肉)
├─ findRecipe ← 匹配 (方块名, 物品名) 规则
├─ checkCooldown ← 连点冷却(防刷提示)
├─ forwardStartTimer ← 计时源方块 → 转发给计时服务启动烤制
├─ checkBelow ← 切换型规则的下方方块检查(烤串需点燃的烧烤炉)
└─ handleSwitch ← 切换型规则具体执行(点火/烤串/上杆/取下)
├─ consumeItem ← 消耗物品
├─ replaceBlock ← 替换方块
├─ giveItem ← 直接给物品(塞背包)
└─ dropItems ← 掉落物形式给物品(取下整只成品)
================================================================================
"""
import time
@@ -33,9 +47,6 @@ comp = serverApi.GetEngineCompFactory() # 组件工厂
from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ....BlueFoxTools.ToolsConfig import LevelId
# ---- 方块实体数据组件(计时持久化:把开始时间戳+时长写进方块,重启后继续计时) ----
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
# ---- 只导入烧烤自己的数据库 ----
from ..BarbecueData import BARBECUE_SWITCHES, ITEM_USE_ON_RULES
@@ -55,31 +66,30 @@ FACE_OFFSET = {
class BarbecueService(BlueFoxServerSystem):
"""
烧烤引擎
烧烤·右键转换引擎
【核心数据结构】
barbecueRecipes = {
(方块名, 物品名): 规则dict, # O(1) 查找
}
barbecueBlockSet = {方块名, ...} # O(1) 拦截/注册用
barbecueTimers = {方块名: 规则dict} # 计时型起始方块 → 规则
itemUseOnRecipes = {物品名: 规则dict} # 手持物品右键方块表面 → 生成规则
barbecueRecipes = {(方块名, 物品名): 规则dict} # O(1) 查找,含 switch+timer 规则
barbecueBlockSet = {方块名, ...} # O(1) 拦截/注册用(所有源方块)
itemUseOnRecipes = {物品名: 规则dict} # 手持物品右键方块表面 → 生成规则
cooldown = {key: 时间戳} # 连点冷却表
"""
def __init__(self, namespace, systemName):
"""
初始化:构建规则字典,并注册到统一分发器(让分发器把右键事件转发过来)。
"""
super(BarbecueService, self).__init__(namespace, systemName)
print "===== BarbecueService init ====="
# ---- 构建烧烤规则字典(O(1) 查找) ----
# switch 和 timer 规则都收进来:右键"计时源方块"时才能识别并转发给计时服务
self.barbecueRecipes = {}
self.barbecueBlockSet = set()
self.barbecueTimers = {}
for sw in BARBECUE_SWITCHES:
key = (sw["source"], sw.get("item"))
self.barbecueRecipes[key] = sw
self.barbecueBlockSet.add(sw["source"])
if sw.get("type") == "timer":
self.barbecueTimers[sw["source"]] = sw
# ---- 手持物品右键方块表面规则:物品名 → 规则 ----
self.itemUseOnRecipes = {}
@@ -93,7 +103,7 @@ class BarbecueService(BlueFoxServerSystem):
FeatureDispatcher.register(self)
# ============================================================================
# 给 FeatureDispatcher 的接口
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getFeatureBlocks(self):
@@ -107,8 +117,10 @@ class BarbecueService(BlueFoxServerSystem):
def handleBlockUse(self, args):
"""
由 FeatureDispatcher 转发的右键方块入口。
只有 blockName 在本功能方块集合里才会被调用。
返回 True=已处理,False=当前组合无匹配规则。
只有 blockName 在本功能的方块集合里才会被调用。
返回 True = 本功能处理了这个右键(分发器会取消默认行为)
返回 False = 当前组合没有匹配规则(分发器不拦截,交还默认行为)
"""
blockName = args.get("blockName")
playerId = args.get("playerId")
@@ -127,23 +139,18 @@ class BarbecueService(BlueFoxServerSystem):
if not self.checkCooldown(key, recipe.get("switchCd", 0.5)):
return True
# ---- 下方方块检查(timer 需篝火,烤串需点燃的烧烤炉) ----
if not self.checkBelow(recipe, dim, pos):
# 下方不是要求的方块 → 失败提示,不执行(但也算处理,取消默认行为)
if recipe.get("type") == "timer":
tip = "§c支杆下方需要放一个篝火(营火)才能烤制!"
else:
tip = "§c下方不是点燃的烧烤炉,无法翻烤!"
comp.CreateMsg(playerId).NotifyOneMessage(playerId, tip)
return True
# 计时型:启动计时
# 计时型规则:右键"计时源方块"(如烤鸡(生))→ 转发给计时服务启动烤制。
# 下方篝火检查在 BarbecueTimerService.startTimer 里做(失败会提示玩家)
if recipe.get("type") == "timer":
self.startTimer(key, recipe, dim, playerId)
return True
return self.forwardStartTimer(blockName, dim, pos, playerId)
# 切换型
self.handleSwitch(recipe, playerId, pos, dim, itemDict, key, blockName)
# 切换型规则:先检查下方方块(烤串要求下方是点燃的烧烤炉)
if not self.checkBelow(recipe, dim, pos):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§c下方不是点燃的烧烤炉,无法翻烤!")
return True # 已拦截并提示,取消默认行为
# 切换型规则:就地执行
self.handleSwitch(recipe, playerId, pos, dim, itemDict, key)
return True
def handleItemUseOn(self, args):
@@ -188,53 +195,75 @@ class BarbecueService(BlueFoxServerSystem):
return True
# ============================================================================
# 规则匹配(O(1))
# 二、规则匹配(O(1) 字典查找)
# ============================================================================
def findRecipe(self, blockName, itemDict):
"""
在烧烤字典中查找匹配规则:
1. 先精确匹配 (方块名, 物品名)
2. 没找到再匹配 (方块名, None) —— 空手也能触发的规则(烤串/取下等)
"""
itemName = itemDict.get("newItemName") if itemDict else None
recipe = self.barbecueRecipes.get((blockName, itemName))
if recipe:
return recipe
recipe = self.barbecueRecipes.get((blockName, None))
if recipe:
return recipe
return None
return self.barbecueRecipes.get((blockName, None))
# ============================================================================
# 切换型规则处理
# 三、计时转发(把"计时源方块"的烤制启动任务交给 BarbecueTimerService)
# ============================================================================
def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key, blockName):
def forwardStartTimer(self, blockName, dim, pos, playerId):
"""
正向(item 非 None):点火/上杆 → 消耗 → 替换 → 给奖励 → 续计时
逆向(item 为 None):烤串2次/取下 → 掉落 → 替换
右键碰到"计时源方块"(如烤鸡(生))时,把启动烤制转交给计时服务。
通过 FeatureDispatcher.startTimerFor 按方块名 O(1) 路由到 BarbecueTimerService。
"""
# ---- 正向 ----
return FeatureDispatcher.startTimerFor(blockName, dim, pos, playerId)
# ============================================================================
# 四、切换型规则处理
# ============================================================================
def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key):
"""
处理"右键直接转换"类规则。
正向(item 不为 None):点火/上杆 → 消耗 → 替换 → 给奖励
逆向(item 为 None) :烤串2次/取下 → 掉落 → 替换
注意:上杆/点火后【不自动启动烤制】。
烧烤计时规则 autoStart=False,必须玩家右键"计时源方块"才激活烤制
(见 handleBlockUse → forwardStartTimer)。
"""
# ---- 正向操作 ----
if recipe.get("item") is not None:
# 消耗物品
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 recipe.get("giveItem"):
self.giveItem(playerId, recipe["giveItem"], recipe.get("giveCount", 1))
# 注意:上杆/点火后【不自动启动烤制】。
# 烤制规则 autoStart=False,必须玩家右键烤制方块才激活计时(见 handleBlockUse)
# 转换成功提示(点火/上杆等)
if recipe.get("tip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["tip"])
return
# ---- 逆向 ----
# ---- 逆向操作 ----
# 掉落物形式生成(取下整只成品等),优先于 giveItem
if recipe.get("dropItem"):
self.dropItems(dim, pos, recipe["dropItem"])
elif recipe.get("giveItem"):
self.giveItem(playerId, recipe["giveItem"], recipe.get("giveCount", 1))
# 替换方块(如烤熟成品取下后还原为支杆)
if recipe.get("target"):
self.replaceBlock(pos, dim, recipe["target"], playerId)
@@ -243,13 +272,14 @@ class BarbecueService(BlueFoxServerSystem):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["tip"])
# ============================================================================
# 计时型规则处理(方块实体 tick 持久化计时,启动前检查下方篝火)
# 五、下方方块检查(切换型规则专用,如烤串要求下方是点燃的烧烤炉)
# ============================================================================
def checkBelow(self, recipe, dim, pos):
"""
检查方块下方是否为 recipe["below"] 中指定的方块。
没写 below 字段 → 视为满足。
(计时型烤制规则的篝火检查在 BarbecueTimerService 里做)
"""
below = recipe.get("below")
if not below:
@@ -261,139 +291,8 @@ class BarbecueService(BlueFoxServerSystem):
except Exception:
return False
def startTimer(self, key, recipe, dim, playerId=None):
"""
启动烤制计时:把 计时标识+开始时间戳+时长 写入方块实体数据。
- 已处于本规则计时中的方块不会重复重置时间
- 到点判断由 FeatureDispatcher 转发的 ServerBlockEntityTickEvent 完成
- 时间戳存在方块上 → 服务器重启/玩家重进后继续计时
(例如烤了40秒退出,回来只剩20秒)
"""
pos = (key[1], key[2], key[3])
# 去重:已在本规则计时中的方块不重置
try:
data = blockEntityDataComp.GetBlockEntityData(dim, pos)
if data:
if data["rtdd_tid"] == recipe["source"]:
return
except Exception:
pass
self._writeTimerData(dim, pos, recipe, playerId)
# 计时开始提示
if playerId and recipe.get("startTip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"])
def handleBlockEntityTick(self, args):
"""FeatureDispatcher 转发:烤制方块每 tick 检查是否到点"""
dim = args.get("dimension")
pos = (args.get("posX"), args.get("posY"), args.get("posZ"))
blockName = args.get("blockName", "")
self._tickProcess(dim, pos, blockName)
def _tickProcess(self, dim, pos, blockName):
"""
tick 主逻辑:
1) 按事件自带的当前方块名判断是否计时源(创造直接放置也会进来)
2) 是计时源但没有计时数据 → 自动补写计时(放置即开始计时)
3) 有计时数据 → 到点执行转换(重启后时间戳照常生效)
方块被拆掉后 tick 停止、数据随方块消失,不会在原位置继续计时转换。
注意:GetBlockEntityData 返回的对象不支持 .get(),这里统一用 [] 读取。
"""
try:
recipe = self.barbecueTimers.get(blockName)
if not recipe:
return # 当前方块不是计时源(tick 只对计时源方块开启)
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
# 读取计时标识;没有或与当前方块不符
try:
tid = blockData["rtdd_tid"]
except Exception:
tid = ""
if tid != blockName:
# 烧烤必须右键激活烤制:autoStart=False 时不自动补写计时
if recipe.get("autoStart", True):
self._writeTimerData(dim, pos, recipe, None)
return
tstart = blockData["rtdd_tstart"]
tdur = blockData["rtdd_tdur"]
if time.time() - tstart >= tdur:
self._finishTimer(dim, pos, recipe, blockData)
except Exception:
pass
def _finishTimer(self, dim, pos, recipe, data):
"""
计时到点:确认方块仍是计时源、下方仍有篝火,才执行转换。
- 篝火被拿走 → 暂停烤制,等篝火回来继续
- 烤熟后清除计时数据,并给启动者发送"烤熟了"提示
"""
try:
cur = comp.CreateBlockInfo(LevelId).GetBlockNew(pos, dim)
if cur and cur.get("name") != recipe["source"]:
self._clearTimerData(dim, pos)
return
if not self.checkBelow(recipe, dim, pos):
return # 篝火被拿走了,暂停烤制
self.replaceBlock(pos, dim, recipe["target"], None)
nextRecipe = self.barbecueTimers.get(recipe["target"])
if nextRecipe:
self._writeTimerData(dim, pos, nextRecipe, data["rtdd_tp"])
else:
self._clearTimerData(dim, pos)
doneTip = recipe.get("doneTip")
if doneTip:
self._notifyIfOnline(data["rtdd_tp"], doneTip)
except Exception:
pass
# ---- 计时数据读写(存在方块实体数据里,重启不丢) ----
def _writeTimerData(self, dim, pos, recipe, playerId=None):
"""把计时信息写进方块实体数据"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
now = time.time()
blockData["rtdd_tid"] = recipe["source"]
blockData["rtdd_tstart"] = now
blockData["rtdd_tdur"] = recipe.get("time", 60)
blockData["rtdd_tp"] = playerId if playerId else ""
except Exception:
pass
def _clearTimerData(self, dim, pos):
"""清除方块上的计时数据"""
try:
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
blockData["rtdd_tid"] = ""
blockData["rtdd_tstart"] = 0
blockData["rtdd_tdur"] = 0
blockData["rtdd_tp"] = ""
except Exception:
pass
def _notifyIfOnline(self, playerId, tip):
"""若启动者在线,发送完成提示(离线则跳过)"""
if not tip or not playerId:
return
try:
ids = comp.CreateGame(LevelId).GetAllPlayerIds()
if playerId in ids:
comp.CreateMsg(playerId).NotifyOneMessage(playerId, tip)
except Exception:
pass
# ============================================================================
# 工具方法
# 六、工具方法:获取玩家主手物品
# ============================================================================
def getCarriedItem(self, playerId):
@@ -401,6 +300,10 @@ class BarbecueService(BlueFoxServerSystem):
return itemComp.GetPlayerItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
# ============================================================================
# 七、工具方法:消耗物品
# ============================================================================
def consumeItem(self, playerId, itemDict, amount=1):
if itemDict is None:
return
@@ -417,7 +320,16 @@ class BarbecueService(BlueFoxServerSystem):
"count": itemDict["count"],
"auxValue": itemDict.get("newAuxValue", 0)})
# ============================================================================
# 八、工具方法:替换方块
# ============================================================================
def replaceBlock(self, pos, dim, newBlockName, playerId, aux=None):
"""
替换指定位置的方块。
newBlockName="minecraft:air" 表示清除该方块。
playerId 可为 None(计时回调时没有玩家),用 LevelId 创建组件。
"""
creator = playerId if playerId is not None else LevelId
if aux is None:
try:
@@ -431,12 +343,23 @@ class BarbecueService(BlueFoxServerSystem):
oldBlockHandling=0,
dimensionId=dim)
# ============================================================================
# 九、工具方法:给玩家物品(塞背包)
# ============================================================================
def giveItem(self, playerId, itemName, count=1, auxValue=0):
comp.CreateItem(playerId).SpawnItemToPlayerInv(
{"itemName": itemName, "count": count, "auxValue": auxValue}, playerId)
# ============================================================================
# 十、工具方法:以掉落物形式生成物品(取下烤鸡/烤乳猪等)
# ============================================================================
def dropItems(self, dim, pos, dropList):
"""以掉落物形式生成物品(取下烤鸡/烤乳猪等)"""
"""
dropList 格式:[{"name": "xxx", "count": N}, ...]
在方块上方生成掉落物实体,让它自然掉到地上。
"""
for d in dropList:
itemDict = {
"itemName": d["name"],
@@ -445,8 +368,15 @@ class BarbecueService(BlueFoxServerSystem):
}
self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2]))
# ============================================================================
# 十一、工具方法:冷却检查
# ============================================================================
def checkCooldown(self, key, cd=0.5):
"""右键冷却:同一方块 cd 秒内只响应一次,防止连点刷物品"""
"""
右键冷却:同一方块 cd 秒内只响应一次,防止连点刷物品/刷提示。
烤串"右键2次"用 switchCd 调小冷却以便快速连点。
"""
now = time.time()
last = self.cooldown.get(key, 0)
if now - last < cd: