This commit is contained in:
wenhe committed 2026-09-11 11:17:45 +08:00
1 parent 4e7b9abee5
commit 052e4844b2
26 files changed
+1053 -41

No files matched your search

@@ -0,0 +1,337 @@
# -*- coding: utf-8 -*-
"""
================================================================================
面食.甜食·右键转换引擎(服务端)
================================================================================
处理"面食.甜食"(xy_mianshi) 分类下的右键转换:
面粉·面团(和面/擀面/切分)、饺子、包子、馍、煎饼卷、竹筒饭、
面包堆叠/递减、面团胚料循环、甜点蛋糕(脱模/加料/切分)。
【事件监听在哪?】
本类【不】监听任何事件!
所有事件由 FeatureDispatcher 统一监听, 按 blockOwner/itemUseOnOwner
(多服务 fallback) 路由到本类 handleBlockUse / handleItemUseOn。
【本类怎么做】
handleBlockUse ← 入口:分发器转发右键方块事件
├─ _handleKnead ← 揉面/和面 连点计数(空手)
├─ _handleAction ← 特殊动作(蒸笼取出/切小面团/切生面/蛋糕脱模)
└─ handleSwitch ← 通用转换(加料/擀面/卷制/堆叠/递减/切分)
handleItemUseOn ← 入口:生面饼右键烤炉烙制 / 面粉(加鸡蛋)右键木碗做面糊
(通用工具) getCarriedItem / consumeItem / giveItem / dropItem / replaceBlock /
clearBlock / checkCooldown
================================================================================
"""
import time
import mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂
# ---- 继承 BlueFox 框架的系统基类(提供 CreateEngineItemEntity 等方法) ----
from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ....BlueFoxTools.ToolsConfig import LevelId
# ---- 只导入面食自己的数据库 ----
from ..MianshiData import (
CLEAVER, ROLLINGPIN, BOWL, OVEN,
MEAT_ITEMS, FILLING_ITEMS, RICH_MEAT_ITEMS,
SWITCH_RULES, KNEAD_COUNTERS, ITEM_USE_ON_RULES,
)
# ---- 统一分发器 ----
from ...service.FeatureDispatcher import FeatureDispatcher
class MianshiService(BlueFoxServerSystem):
"""
面食.甜食·右键转换引擎
"""
def __init__(self, namespace, systemName):
super(MianshiService, self).__init__(namespace, systemName)
print "===== MianshiService init ====="
# ---- (方块, 手持物品) -> 规则: O(1) 查找 ----
self.recipes = {}
self.featureBlocks = set()
def add(rule):
block = rule["block"]
item = rule.get("item")
self.recipes[(block, item)] = rule
self.featureBlocks.add(block)
for r in SWITCH_RULES:
add(r)
# 竹筒饭加肉: (bamboo_2 + 任一肉类) -> bamboo_4
for meat in MEAT_ITEMS:
add({"block": "rtdd:bamboo_2", "item": meat,
"target": "rtdd:bamboo_4", "consume": 1, "tip": "§a竹筒加了肉"})
# 揉面/和面连点方块也要注册(空手右键路由)
for b in KNEAD_COUNTERS:
self.featureBlocks.add(b)
# ---- itemUseOn 映射: 手持物品 -> 规则 ----
self.itemUseOnMap = {}
for r in ITEM_USE_ON_RULES:
self.itemUseOnMap[r["item"]] = r
# ---- 揉面/和面计数 {(dim,x,y,z): n} ----
self.kneadCount = {}
# ---- 右键冷却表 ----
self.cooldown = {}
# ---- 注册到统一分发器 ----
FeatureDispatcher.register(self)
# ============================================================================
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getFeatureBlocks(self):
"""返回本功能管辖的所有右键方块名集合"""
return self.featureBlocks
def getItemUseOnRules(self):
"""手持物品右键方块表面路由: 生面饼(烙制) / 面粉(加鸡蛋)(做面糊)"""
return {r["item"]: True for r in ITEM_USE_ON_RULES}
def handleBlockUse(self, args):
"""由 FeatureDispatcher 转发的右键方块入口"""
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)
itemName = itemDict.get("newItemName") if itemDict else None
# ---- 揉面/和面: 空手右键连点 ----
if blockName in KNEAD_COUNTERS and (not itemName or itemName == "minecraft:air"):
return self._handleKnead(blockName, playerId, pos, dim, key)
# ---- 常规转换 ----
recipe = self.findRecipe(blockName, itemDict)
if not recipe:
return False
if not self.checkCooldown(key, 0.4):
return True
self.handleSwitch(recipe, playerId, pos, dim, itemDict)
return True
def handleItemUseOn(self, args):
"""由 FeatureDispatcher 转发的"手持物品右键方块表面"入口:
1) 手持 生面饼 右键 烤炉 -> 烤炉上方放置 馍(烙制), 消耗1个生面饼
2) 手持 面粉(加鸡蛋) 右键 木碗 -> 木碗变 蛋糕面糊(生), 消耗1个
返回 True=处理了; False=不属本功能管(交还默认行为)。"""
itemName = args.get("itemName")
playerId = args.get("playerId") or args.get("entityId")
x, y, z = args.get("x"), args.get("y"), args.get("z")
dim = comp.CreateDimension(playerId).GetEntityDimensionId()
rule = self.itemUseOnMap.get(itemName)
if not rule:
return False
# 被点方块必须是规则要求的表面方块
try:
cur = comp.CreateBlockInfo(LevelId).GetBlockNew((x, y, z), dim)
if not cur or cur.get("name") != rule["surface"]:
return False
except Exception:
return False
if not self.checkCooldown((dim, x, y, z), 0.4):
return True
itemDict = self.getCarriedItem(playerId)
if not itemDict:
return False
self.consumeItem(playerId, itemDict, rule.get("consume", 1))
if rule.get("above"):
# 在表面方块上方放置结果(烙制)
targetPos = (x, y + 1, z)
try:
cur = comp.CreateBlockInfo(LevelId).GetBlockNew(targetPos, dim)
if cur and cur.get("name") != "minecraft:air":
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§c上方被占, 无法烙制")
return True
except Exception:
pass
self.replaceBlock(targetPos, dim, rule["above"], playerId)
elif rule.get("replace"):
# 表面方块本身被替换(木碗变蛋糕面糊)
self.replaceBlock((x, y, z), dim, rule["replace"], playerId)
if rule.get("tip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, rule["tip"])
return True
# ============================================================================
# 二、规则匹配(精确 + 空手 fallback)
# ============================================================================
def findRecipe(self, blockName, itemDict):
itemName = itemDict.get("newItemName") if itemDict else None
recipe = self.recipes.get((blockName, itemName))
if recipe:
return recipe
if not itemName or itemName == "minecraft:air":
return self.recipes.get((blockName, None))
return None
# ============================================================================
# 三、揉面/和面(空手右键连点)
# ============================================================================
def _handleKnead(self, blockName, playerId, pos, dim, key):
if not self.checkCooldown(key, 0.4):
return True
cfg = KNEAD_COUNTERS[blockName]
n = self.kneadCount.get(key, 0) + 1
if n >= cfg["times"]:
self.kneadCount.pop(key, None)
self.replaceBlock(pos, dim, cfg["target"], playerId)
comp.CreateMsg(playerId).NotifyOneMessage(playerId, cfg.get("doneTip", "§a完成"))
else:
self.kneadCount[key] = n
comp.CreateMsg(playerId).NotifyOneMessage(playerId, cfg["tip"] % n)
return True
# ============================================================================
# 四、通用转换执行
# ============================================================================
def handleSwitch(self, recipe, playerId, pos, dim, itemDict):
# 消耗物品
if recipe.get("consume", 0) > 0:
self.consumeItem(playerId, itemDict, recipe["consume"])
# 替换方块
if recipe.get("target"):
self.replaceBlock(pos, dim, recipe["target"], playerId)
# 特殊动作(自行管理掉落, 避免与通用掉落重复)
action = recipe.get("action")
# 掉落物(切分/递减/取出) —— 无特殊动作时才走通用掉落
if recipe.get("dropItem") and not action:
self.dropItem(dim, pos, recipe["dropItem"])
# 给背包物品(水桶变空桶/擀面杖返还)
if recipe.get("give"):
self.giveItem(playerId, recipe["give"])
# 特殊动作
if action == "chop_smalldough":
self.clearBlock(pos, dim)
for _ in range(8):
self.dropItem(dim, pos, "rtdd:smalldough_0")
elif action == "chop_noodles":
self.clearBlock(pos, dim)
self.dropItem(dim, pos, "rtdd:noodles")
elif action == "unmold_cake":
# 蛋糕面糊(烤熟) 脱模 -> 蛋糕胚(方块) + 掉落 1 木碗
self.replaceBlock(pos, dim, "rtdd:cakebase", playerId)
self.dropItem(dim, pos, BOWL)
elif action == "split_cake":
# 半块蛋糕 菜刀拆分: 半块消失 -> 掉落两个对应小块
self.clearBlock(pos, dim)
for _ in range(2):
self.dropItem(dim, pos, recipe["dropItem"])
elif action == "take_steamer":
# 蒸笼取出: 蒸食物块 -> 变回空蒸笼 + 掉落成品
self.replaceBlock(pos, dim, "rtdd:steamer", playerId)
if recipe.get("dropItem"):
self.dropItem(dim, pos, recipe["dropItem"])
# 提示
if recipe.get("tip"):
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["tip"])
# ============================================================================
# 五、工具方法:获取玩家主手物品
# ============================================================================
def getCarriedItem(self, playerId):
itemComp = comp.CreateItem(playerId)
return itemComp.GetPlayerItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0, True)
# ============================================================================
# 六、工具方法:给玩家物品
# ============================================================================
def giveItem(self, playerId, itemName, count=1, aux=0):
try:
itemDict = {"itemName": itemName, "count": count, "auxValue": aux}
comp.CreateItem(playerId).SpawnItemToPlayerInv(itemDict, playerId)
except Exception:
pass
# ============================================================================
# 七、工具方法:消耗物品
# ============================================================================
def consumeItem(self, playerId, itemDict, amount=1):
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:
itemDict["count"] = count - amount
comp.CreateItem(playerId).SetEntityItem(
serverApi.GetMinecraftEnum().ItemPosType.CARRIED,
{"itemName": itemDict["newItemName"],
"count": itemDict["count"],
"auxValue": itemDict.get("newAuxValue", 0),
"userData": itemDict.get("userData", {})})
# ============================================================================
# 八、工具方法:以掉落物形式生成物品
# ============================================================================
def dropItem(self, dim, pos, itemName, count=1, aux=0):
try:
itemDict = {"itemName": itemName, "count": count, "auxValue": aux}
self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2]))
except Exception:
pass
# ============================================================================
# 九、工具方法:替换方块 / 清空方块(保留 aux, 兼容定向变体)
# ============================================================================
def replaceBlock(self, pos, dim, newBlockName, playerId=None):
creator = playerId if playerId is not None else LevelId
aux = 0
try:
oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim)
aux = oldBlock.get("aux", 0)
except Exception:
aux = 0
comp.CreateBlockInfo(creator).SetBlockNew(
pos, blockDict={"name": newBlockName, "aux": aux},
oldBlockHandling=0, dimensionId=dim)
def clearBlock(self, pos, dim):
comp.CreateBlockInfo(LevelId).SetBlockNew(
pos, blockDict={"name": "minecraft:air", "aux": 0},
oldBlockHandling=0, dimensionId=dim)
# ============================================================================
# 十、工具方法:冷却检查
# ============================================================================
def checkCooldown(self, key, cd=0.4):
now = time.time()
last = self.cooldown.get(key, 0)
if now - last < cd:
return False
self.cooldown[key] = now
return True
@@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
"""
================================================================================
面食.甜食·计时转换引擎(服务端)
================================================================================
只处理"面食.甜食"(xy_mianshi) 分类下的【计时自动转换】:
发酵60秒(和面_4 -> 发酵后 / 法棍面胚_1 -> 发面)
蒸笼蒸熟60秒(食物放蒸笼上 1秒入蒸笼, 再60秒蒸熟)
烤炉烘焙60秒(下方判定烤炉: 胚料/面糊/蛋液 放烤炉上计时)
【与右键转换解耦】
右键转换(和面/擀面/加料/蒸笼取出等)在 MianshiService, 本类只负责计时。
【烤炉机制(用户指定)】
- 下方方块判定为 烤炉(oven) 才对该方块计时, 烤炉只做判断用
- 计时期间 烤炉被打碎 -> 终止计时, 烤炉上方方块 变成掉落物, 并给出提示
【下方判定】
below: 该方块的"正下方一格"必须是指定方块(烤炉/蒸笼)才计时
dropOnBreak: 下方判定失败且计时中 -> 方块掉落+提示(烤炉); 否则暂停(蒸笼)
【事件监听在哪?】
本类【不】监听任何事件!
ServerBlockEntityTickEvent 由 FeatureDispatcher 统一监听, 再按 timerOwner
路由到本类的 handleBlockEntityTick。计时用绝对时间戳写方块实体数据,
退出重进继续计时。
================================================================================
"""
import time
import mod.server.extraServerApi as serverApi
# ---- 网易API基础对象 ----
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
comp = serverApi.GetEngineCompFactory() # 组件工厂
# ---- 继承 BlueFox 框架的系统基类 ----
from ....BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
from ....BlueFoxTools.ToolsConfig import LevelId
# ---- 方块实体数据组件(计时持久化: 开始时间戳+时长写进方块) ----
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
# ---- 只导入面食自己的数据库 ----
from ..MianshiData import TIMER_RULES, BREAK_TIP
# ---- 统一分发器 ----
from ...service.FeatureDispatcher import FeatureDispatcher
class MianshiTimerService(BlueFoxServerSystem):
"""
面食.甜食·计时转换引擎
"""
def __init__(self, namespace, systemName):
super(MianshiTimerService, self).__init__(namespace, systemName)
print "===== MianshiTimerService init ====="
# ---- 计时规则: source -> [规则...] (同一source可对应蒸笼/烤炉等不同下方) ----
self.timerMap = {}
for t in TIMER_RULES:
self.timerMap.setdefault(t["source"], []).append(t)
# ---- 注册到分发器的计时路由表 ----
FeatureDispatcher.registerTimer(self)
# ============================================================================
# 一、给 FeatureDispatcher 的接口
# ============================================================================
def getTimerBlocks(self):
"""返回本功能的所有"计时源方块"集合(tick 路由用)"""
return set(self.timerMap.keys())
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)
# ============================================================================
# 二、启动计时(本模块 autoStart, 保留接口以便需要时外部触发)
# ============================================================================
def startTimer(self, dim, pos, blockName, playerId=None):
rules = self.timerMap.get(blockName)
if not rules:
return False
rule = self._pickRule(rules, dim, pos)
if not rule:
return False
try:
data = blockEntityDataComp.GetBlockEntityData(dim, pos)
if data and data["rtdd_tid"] == blockName:
return False
except Exception:
pass
self._writeTimerData(dim, pos, rule, playerId)
return True
# ============================================================================
# 三、tick 检查与到点转换
# ============================================================================
def _pickRule(self, rules, dim, pos):
"""按"下方方块"匹配规则(蒸笼/烤炉等), 返回命中的规则; 无命中返回 None"""
belowPos = (pos[0], pos[1] - 1, pos[2])
try:
b = comp.CreateBlockInfo(LevelId).GetBlockNew(belowPos, dim)
belowName = b.get("name") if b else ""
except Exception:
belowName = ""
for rule in rules:
if belowName in rule.get("below", [belowName]):
return rule
return None
def _tickProcess(self, dim, pos, blockName):
try:
rules = self.timerMap.get(blockName)
if not rules:
return
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
if blockData is None:
return
try:
tid = blockData["rtdd_tid"]
except Exception:
tid = ""
if tid != blockName:
# autoStart: 计时源方块无计时数据且下方方块匹配时自动启动
if any(r.get("autoStart", True) for r in rules):
rule = self._pickRule(rules, dim, pos)
if rule:
self._writeTimerData(dim, pos, rule, None)
return
# 计时中: 按下方判定规则处理(烤炉被打碎 -> 掉落; 蒸笼移走 -> 暂停)
rule = self._pickRule(rules, dim, pos)
if rule is None:
self._handleBelowBroken(dim, pos, blockName, blockData, rules)
return
tstart = blockData["rtdd_tstart"]
tdur = blockData["rtdd_tdur"]
if time.time() - tstart >= tdur:
self._finishTimer(dim, pos, rule, blockData)
except Exception:
pass
def _handleBelowBroken(self, dim, pos, blockName, data, rules):
"""下方判定方块被打碎/移走:
- 烤炉(dropOnBreak): 终止计时 + 方块掉落 + 提示
- 蒸笼: 暂停计时(数据保留, 蒸笼回来继续)"""
rule = None
for r in rules:
if r.get("dropOnBreak"):
rule = r
break
if rule is None:
return # 蒸笼等: 暂停
# 烤炉被打碎: 方块掉落 + 变空气 + 终止计时 + 提示
self._clearTimerData(dim, pos)
self._dropBlockItem(dim, pos, blockName)
self._setAir(dim, pos)
self._notifyIfOnline(data.get("rtdd_tp"), BREAK_TIP)
def _finishTimer(self, dim, pos, rule, data):
try:
cur = comp.CreateBlockInfo(LevelId).GetBlockNew(pos, dim)
if cur and cur.get("name") != rule["source"]:
self._clearTimerData(dim, pos)
return
# 到点转换
self.replaceBlock(pos, dim, rule["target"], None)
self._clearTimerData(dim, pos)
if rule.get("doneTip"):
self._notifyIfOnline(data.get("rtdd_tp"), rule["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 _dropBlockItem(self, dim, pos, blockName):
"""把当前方块以物品实体形式掉落(烤炉被打碎时)"""
try:
itemDict = {"itemName": blockName, "count": 1, "auxValue": 0}
self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2]))
except Exception:
pass
def _setAir(self, dim, pos):
try:
comp.CreateBlockInfo(LevelId).SetBlockNew(
pos, blockDict={"name": "minecraft:air", "aux": 0},
oldBlockHandling=0, dimensionId=dim)
except Exception:
pass
# ============================================================================
# 六、工具方法:替换方块
# ============================================================================
def replaceBlock(self, pos, dim, newBlockName, playerId=None):
creator = playerId if playerId is not None else LevelId
aux = 0
try:
oldBlock = comp.CreateBlockInfo(creator).GetBlockNew(pos, dim)
aux = oldBlock.get("aux", 0)
except Exception:
aux = 0
comp.CreateBlockInfo(creator).SetBlockNew(
pos, blockDict={"name": newBlockName, "aux": aux},
oldBlockHandling=0, dimensionId=dim)
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-