黄豆 饮料 冰棍 实现
This commit is contained in:
1 parent
38f2ff2b33
commit
f89b95fb99
23 files changed
+1773
-88
No files matched your search
@@ -10,6 +10,8 @@ from xiangyemod.shouban.service.ShoubanService import ShoubanService
|
||||
from xiangyemod.shichangmaoyi.service.MarketService import MarketService
|
||||
from xiangyemod.yuanbanfangzhi.service.YuanbanService import YuanbanService
|
||||
from xiangyemod.jiaju.service.FurnitureService import FurnitureService
|
||||
from xiangyemod.yinliao.service.YinliaoService import YinliaoService
|
||||
from xiangyemod.yinliao.service.YinliaoTimerService import YinliaoTimerService
|
||||
|
||||
if not serverApi.IsInServer():
|
||||
pass
|
||||
|
||||
+4
-2
@@ -111,21 +111,23 @@ class AnimalTimerService(BlueFoxServerSystem):
|
||||
启动计时:把 计时标识+开始时间戳+时长 写入方块实体数据。
|
||||
- 已处于本规则计时中的方块不会重复重置时间(保持原剩余时间)
|
||||
- 动物计时 autoStart 默认 True:放置/填充后自动开始(tick 也会自动补写)
|
||||
返回 True=找到规则并处理(含已计时中);False=不属于本计时功能
|
||||
"""
|
||||
recipe = self.animalTimers.get(blockName)
|
||||
if not recipe:
|
||||
return
|
||||
return False
|
||||
# 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间)
|
||||
try:
|
||||
data = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if data and data["rtdd_tid"] == blockName:
|
||||
return
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
self._writeTimerData(dim, pos, recipe, playerId)
|
||||
# 计时开始提示
|
||||
if playerId and recipe.get("startTip"):
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"])
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# 三、tick 检查与到点转换
|
||||
|
||||
@@ -0,0 +1,764 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
================================================================================
|
||||
工具方块切换引擎(服务端)
|
||||
================================================================================
|
||||
|
||||
这个文件负责所有"普通工具方块"的右键转换逻辑,和做菜(CookingService)彻底分开。
|
||||
|
||||
【一句话概括】
|
||||
玩家右键工具方块(箭桶、挂肉钩、水槽、盘子架、药水架、书柜……) →
|
||||
查 BlockSwitchData 有没有匹配的规则 → 消耗物品 → 替换方块 → 保存记录
|
||||
|
||||
【为什么单独拆一个类】
|
||||
原来做菜和工具切换都在 CookingService 里,右键时每次都要同时碰到两套数据,
|
||||
内存和逻辑都搅在一起。现在拆成两个系统:
|
||||
- CookingService → 只处理做菜配方(RECIPES)
|
||||
- BlockSwitchService → 只处理工具方块切换(BLOCK_SWITCHES)
|
||||
两个系统都监听同一个 ServerBlockUseEvent,
|
||||
但各自在事件第一行用 switchBlockSet/cookBlockSet(O(1) 的 set)按方块名拦截:
|
||||
不是自己的方块直接 return,互不干扰,各自只查自己那一份数据。
|
||||
|
||||
【事件监听】
|
||||
- ServerBlockUseEvent → 玩家右键方块(工具切换主入口)
|
||||
- ServerItemTryUseEvent → 玩家尝试使用物品(拦截喝药水等默认行为)
|
||||
|
||||
【保存/持久化】
|
||||
箭桶、药水架、书柜这类有 record 记录的方块,把放进来的物品记录在
|
||||
方块实体数据里(字段 rtdd_swt/rtdd_swc/rtdd_swi/rtdd_sws),
|
||||
这样玩家退出游戏再重进,loadSwitchRecord 还能从方块里把物品读回来。
|
||||
================================================================================
|
||||
"""
|
||||
import time
|
||||
import mod.server.extraServerApi as serverApi
|
||||
|
||||
# ---- 网易API基础对象 ----
|
||||
ServerSystem = serverApi.GetServerSystemCls() # 服务端系统基类
|
||||
comp = serverApi.GetEngineCompFactory() # 组件工厂,用来创建各种功能组件
|
||||
|
||||
# ---- 继承 BlueFox 框架的系统基类 ----
|
||||
# BlueFoxServerSystem 会自动处理 LoadMod / UnloadMod 等生命周期,
|
||||
# 不需要手动写,继承后框架自动发现并注册(ModLoader 的 GetLastSubclass)
|
||||
from ...BlueFoxTools.SpecialFunciton.BlueFoxSystem import BlueFoxServerSystem
|
||||
from ...BlueFoxTools.ToolsConfig import LevelId
|
||||
|
||||
# ---- 方块实体数据组件 ----
|
||||
# GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作,直接赋值即可持久化。
|
||||
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
|
||||
|
||||
# ---- 游戏组件(用于 AddTimer 延迟写入 block entity) ----#
|
||||
gameComp = comp.CreateGame(LevelId)
|
||||
|
||||
|
||||
# ---- 导入工具方块切换规则 ----
|
||||
# BLOCK_SWITCHES 是一个列表,每条规则是一个 dict,格式见 BlockSwitchData.py 顶部注释
|
||||
from ..BlockSwitchData import BLOCK_SWITCHES
|
||||
|
||||
# ---- 已由 DeadmobService/DeadmobTimerService 接管的死亡生物方块 ----
|
||||
# 这些方块属于"死亡生物"(xy_deadmob) 分类,由新系统处理(挂肉钩切肉/鱼晾干/死鱼切块),
|
||||
# 工具切换系统必须跳过,否则右键会触发旧规则(含逆向退回),和新系统双重响应产生冲突。
|
||||
_DEADMOB_OWNED = set([
|
||||
"rtdd:meathook",
|
||||
"rtdd:cow_0", "rtdd:cow_1", "rtdd:cow_2", "rtdd:cow_3",
|
||||
"rtdd:pig_0", "rtdd:pig_1", "rtdd:pig_2", "rtdd:pig_3",
|
||||
"rtdd:sheep", "rtdd:sheep_1", "rtdd:sheep_2", "rtdd:sheep_3",
|
||||
"rtdd:hang_meat_hooks_1", "rtdd:hang_meat_hooks_2",
|
||||
"rtdd:hang_meat_hooks_3", "rtdd:hang_meat_hooks_4",
|
||||
"rtdd:meathookcod_1", "rtdd:meathookcod_2", "rtdd:meathookcoddry",
|
||||
"rtdd:killfish_0", "rtdd:killfish_1", "rtdd:killfish_2", "rtdd:killfish_3",
|
||||
"rtdd:killcod_0", "rtdd:killcod_1", "rtdd:killcod_2",
|
||||
"rtdd:deadsquida", "rtdd:dead_squid_1",
|
||||
# 旧版挂鱼方块(minecraft:fish 挂肉钩),已被新挂鱼(鲑鱼/鳕鱼)取代
|
||||
"rtdd:fish_1", "rtdd:fish_2",
|
||||
])
|
||||
|
||||
|
||||
# ================================================================================
|
||||
# 工具函数:读取物品 userData 里的嵌套字段
|
||||
# ================================================================================
|
||||
|
||||
def getUserDataField(itemDict, *keys):
|
||||
"""
|
||||
从物品 dict 的 userData 里一层层往下读。
|
||||
中间某一层不存在会自动创建空 dict,不会报错。
|
||||
"""
|
||||
itemDict['userData'] = itemDict.get('userData', {})
|
||||
if itemDict.get('userData') is None:
|
||||
itemDict['userData'] = {}
|
||||
node = itemDict['userData']
|
||||
for key in keys:
|
||||
node[key] = node.get(key, {})
|
||||
node = node[key]
|
||||
return node
|
||||
|
||||
|
||||
# ================================================================================
|
||||
# 工具方块切换引擎主类
|
||||
# ================================================================================
|
||||
|
||||
class BlockSwitchService(BlueFoxServerSystem):
|
||||
"""
|
||||
通用工具方块切换引擎
|
||||
|
||||
【核心数据结构】
|
||||
switchRecord = {
|
||||
(维度ID, x, y, z): { # 用方块坐标当 key,每个方块独立
|
||||
"count": 整数, # recordType="count" 时记录数量(如箭桶里有多少箭)
|
||||
"itemName": 字符串, # recordType="count" 时记录物品名(如"minecraft:arrow")
|
||||
"stack": [物品信息列表], # recordType="stack" 时记录后进先出的物品堆
|
||||
}
|
||||
}
|
||||
|
||||
这个数据同时存在两处:
|
||||
1. 内存(self.switchRecord)—— 快,关服就没了
|
||||
2. 方块实体数据(blockEntityData)—— 慢一点,但关服重启后还在
|
||||
每次操作都会同步更新两处,保证重启不丢记录
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, systemName):
|
||||
"""
|
||||
初始化:清空记录数据、注册事件监听、构建工具切换规则字典
|
||||
"""
|
||||
super(BlockSwitchService, self).__init__(namespace, systemName)
|
||||
print "===== BlockSwitchService init ====="
|
||||
self.switchRecord = {} # 工具方块的记录数据(箭桶数量、药水架顺序等)
|
||||
self.switchCooldown = {} # 工具方块切换冷却时间(防止连点)
|
||||
self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等)
|
||||
self.ListenEvent() # 注册游戏事件监听
|
||||
|
||||
# ---- 构建工具切换规则字典(O(1) 查找) ----
|
||||
# key = (方块名, 物品名),value = 规则dict
|
||||
self.switchRecipes = {}
|
||||
for switch in BLOCK_SWITCHES:
|
||||
key = (switch["source"], switch.get("item"))
|
||||
self.switchRecipes[key] = switch
|
||||
|
||||
# ---- 构建工具方块名集合(O(1) 快速拦截,第一道过滤) ----
|
||||
# 事件进来先判断方块名在不在这个集合里,不在直接 return,
|
||||
# 这样做菜方块的右键事件根本不会进到本系统的字典查找
|
||||
self.switchBlockSet = set()
|
||||
for switch in BLOCK_SWITCHES:
|
||||
self.switchBlockSet.add(switch["source"])
|
||||
|
||||
def Destroy(self):
|
||||
"""
|
||||
系统销毁时取消所有事件监听,防止内存泄漏
|
||||
"""
|
||||
print "===== BlockSwitchService Destroy ====="
|
||||
self.UnListenEvent()
|
||||
|
||||
# ============================================================================
|
||||
# 事件注册与注销
|
||||
# ============================================================================
|
||||
|
||||
def ListenEvent(self):
|
||||
"""
|
||||
注册两个游戏事件:
|
||||
1. ServerBlockUseEvent —— 玩家右键方块(工具切换入口)
|
||||
2. ServerItemTryUseEvent —— 玩家尝试使用物品(拦截喝药水等默认行为)
|
||||
"""
|
||||
# 事件1:玩家右键方块
|
||||
self.ListenForEvent(serverApi.GetEngineNamespace(),
|
||||
serverApi.GetEngineSystemName(),
|
||||
"ServerBlockUseEvent", self, self.OnBlockUse)
|
||||
# 事件2:玩家尝试使用物品(拦截喝药水)
|
||||
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(),
|
||||
"ServerItemTryUseEvent", self, self.OnItemTryUse)
|
||||
|
||||
# ============================================================================
|
||||
# 主入口:玩家右键方块
|
||||
# ============================================================================
|
||||
|
||||
def OnBlockUse(self, args):
|
||||
"""
|
||||
玩家右键方块时触发,是工具方块切换的入口。
|
||||
|
||||
流程:
|
||||
1. 先按方块名判断是不是本系统管辖的工具方块(O(1) set 拦截)
|
||||
- 不是 → 直接 return(这是做菜方块的右键,交给 CookingService 处理)
|
||||
2. 拿到被右键的方块名、玩家ID、方块坐标
|
||||
3. 看玩家手里拿了什么物品
|
||||
4. 拿着"方块名+手里物品名"去工具切换字典里 O(1) 查找
|
||||
5. 命中规则 → 执行工具切换逻辑
|
||||
"""
|
||||
# ---- 第一步:提取事件参数 ----
|
||||
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) # 完整的唯一标识:维度+坐标
|
||||
|
||||
# ---- 第二步:O(1) 方块名拦截 ----
|
||||
# 只有工具方块的右键才归本系统处理;做菜方块直接放过,交给 CookingService
|
||||
if blockName in _DEADMOB_OWNED:
|
||||
# 死亡生物方块由新系统(DeadmobService)处理,本系统跳过
|
||||
return
|
||||
if blockName not in self.switchBlockSet:
|
||||
return
|
||||
|
||||
# ---- 第三步:拿到玩家手里拿的物品 ----
|
||||
itemDict = self.getCarriedItem(playerId)
|
||||
|
||||
# ---- 第四步:查工具切换规则(O(1) 字典查找) ----
|
||||
recipe = self.findRecipe(blockName, itemDict)
|
||||
if not recipe:
|
||||
# 方块是工具方块,但当前组合没有匹配的规则(比如手里物品不对)→ 不拦截
|
||||
return
|
||||
|
||||
# 找到规则后,取消默认行为(如喝药水、吃食物等),由本 Mod 全权接管
|
||||
args['cancel'] = True
|
||||
# 同时标记该玩家下一次物品使用事件也要取消,防止喝药水等默认行为
|
||||
self._cancelItemUse[playerId] = True
|
||||
|
||||
# ---- 第五步:加载方块历史记录到内存 ----
|
||||
# 这样 handleSwitch 里直接从 self.switchRecord 取就行
|
||||
self._loadBlockDataToMemory(key, playerId, pos, dim)
|
||||
|
||||
# ---- 第六步:执行工具切换 ----
|
||||
self.handleSwitch(recipe, playerId, pos, dim, itemDict, key, blockName)
|
||||
|
||||
# ============================================================================
|
||||
# 拦截物品使用事件(防止喝药水等默认行为)
|
||||
# ============================================================================
|
||||
|
||||
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) —— 空手也能触发的规则
|
||||
3. 如果匹配到的是 arrowMode 规则,额外检查手中物品是否为箭
|
||||
|
||||
返回:匹配到的规则 dict,找不到返回 None
|
||||
"""
|
||||
itemName = itemDict.get("newItemName") if itemDict else None
|
||||
|
||||
# 精确匹配:(方块名, 物品名)
|
||||
recipe = self.switchRecipes.get((blockName, itemName))
|
||||
if recipe:
|
||||
return recipe
|
||||
|
||||
# 空手匹配:(方块名, None)
|
||||
recipe = self.switchRecipes.get((blockName, None))
|
||||
if recipe:
|
||||
# 如果规则是 arrowMode 存箭模式,检查手中物品是否为箭
|
||||
if recipe.get("arrowMode") == "store":
|
||||
if itemDict and self._isArrow(itemDict):
|
||||
return recipe
|
||||
return None
|
||||
return recipe
|
||||
|
||||
return None
|
||||
|
||||
def _isArrow(self, itemDict):
|
||||
"""
|
||||
判断手中物品是否为箭(普通箭或药水箭)。
|
||||
返回 True/False。
|
||||
"""
|
||||
if not itemDict:
|
||||
return False
|
||||
itemName = itemDict.get("newItemName", "")
|
||||
return itemName == "minecraft:arrow" or itemName == "minecraft:tipped_arrow"
|
||||
|
||||
# ============================================================================
|
||||
# 工具方块切换处理
|
||||
# ============================================================================
|
||||
|
||||
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
|
||||
|
||||
# ---- 箭桶模式(arrowMode):存箭 / 取箭 ----
|
||||
# 独立于普通 count/stack 逻辑,因为箭桶需要批量操作
|
||||
arrowMode = recipe.get("arrowMode")
|
||||
if arrowMode:
|
||||
self._handleArrowMode(arrowMode, recipe, playerId, pos, dim, itemDict, key, blockName)
|
||||
return
|
||||
|
||||
# ---- 计时型:直接替换方块,不消耗物品,不记录 ----
|
||||
if recipe.get("type") == "timer":
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
return
|
||||
|
||||
# ---- 切换型 ----
|
||||
# 内存中已有数据(_loaBlockDataToMemory 已加载),直接用
|
||||
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"],
|
||||
"auxValue": itemDict.get("newAuxValue", 0),
|
||||
}
|
||||
# 检查是否满了
|
||||
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
|
||||
|
||||
# ============================================================================
|
||||
# 通用持久化帮手(避免重复的 try/except/Get/Set 写法)
|
||||
# ============================================================================
|
||||
|
||||
def _writeBlockData(self, dim, pos, **kwargs):
|
||||
"""
|
||||
通用方块实体数据写入。
|
||||
|
||||
GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作,
|
||||
直接赋值即可持久化。官方没有 SetBlockEntityData 方法。
|
||||
"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
return
|
||||
for key, value in kwargs.items():
|
||||
blockData[key] = value
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
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):
|
||||
"""
|
||||
在确定玩家右键命中了一个工具切换规则后,立刻把方块里存的记录加载到内存。
|
||||
|
||||
这样 handleSwitch 里就不用自己加载了,直接从 self.switchRecord 取就行。
|
||||
"""
|
||||
if key not in self.switchRecord:
|
||||
switchInfo = self.loadSwitchRecord(playerId, pos, dim)
|
||||
if switchInfo:
|
||||
self.switchRecord[key] = switchInfo
|
||||
|
||||
# ============================================================================
|
||||
# 工具方块记录持久化(药水架、书柜、箭桶等)
|
||||
# ============================================================================
|
||||
|
||||
def saveSwitchRecord(self, playerId, pos, dim, record):
|
||||
"""
|
||||
把工具方块的记录数据写入方块实体数据。
|
||||
记录类型:count型(箭桶)→ rtdd_swt/swc/swi,stack型(书柜/药水架)→ rtdd_swt/sws
|
||||
|
||||
记录存在方块实体数据里,所以玩家退出游戏再重进,
|
||||
loadSwitchRecord 还能从方块里把物品读回来。
|
||||
"""
|
||||
if "count" in record:
|
||||
self._writeBlockData(dim, pos,
|
||||
rtdd_swt="count",
|
||||
rtdd_swc=record["count"],
|
||||
rtdd_swi=record.get("itemName", ""),
|
||||
rtdd_swa=record.get("auxValue", 0))
|
||||
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)
|
||||
if blockData is None:
|
||||
return None
|
||||
swt = blockData["rtdd_swt"]
|
||||
if swt == "count":
|
||||
count = blockData["rtdd_swc"]
|
||||
itemName = blockData["rtdd_swi"]
|
||||
try:
|
||||
auxValue = blockData["rtdd_swa"]
|
||||
except (KeyError, Exception):
|
||||
auxValue = 0
|
||||
return {
|
||||
"count": count,
|
||||
"itemName": itemName,
|
||||
"auxValue": auxValue,
|
||||
}
|
||||
elif swt == "stack":
|
||||
stack = blockData["rtdd_sws"]
|
||||
return {
|
||||
"stack": stack,
|
||||
}
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
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_swa=0, rtdd_sws=[])
|
||||
|
||||
# ============================================================================
|
||||
# 箭桶模式(arrowMode):存箭 / 取箭
|
||||
# ============================================================================
|
||||
|
||||
def _handleArrowMode(self, arrowMode, recipe, playerId, pos, dim, itemDict, key, blockName):
|
||||
"""
|
||||
处理箭桶的存箭/取箭逻辑。
|
||||
|
||||
【存箭流程】
|
||||
1. 检查手中是否为箭(_isArrow 已在 findRecipe 验证过)
|
||||
2. 检查方块是否有记录且类型匹配:
|
||||
a. 已有记录 → 检查手中箭类型(itemName+auxValue)是否匹配 → 不匹配则 return
|
||||
b. 无记录 → 新建记录
|
||||
3. 计算实际可存数量 = min(手中数量, maxCount - 已有数量)
|
||||
4. 如果可存数量 <= 0 → return
|
||||
5. 从手中扣减,更新记录,替换方块
|
||||
|
||||
【取箭流程】
|
||||
1. 检查方块是否有记录且 count > 0
|
||||
2. 给玩家手中/背包添加 count 只箭(itemName + auxValue 按记录)
|
||||
3. 清空记录,替换方块
|
||||
"""
|
||||
record = self.switchRecord.get(key)
|
||||
|
||||
if arrowMode == "store":
|
||||
# ---- 存箭 ----
|
||||
handCount = itemDict.get("count", 0)
|
||||
if handCount <= 0:
|
||||
return
|
||||
|
||||
handItemName = itemDict.get("newItemName", "")
|
||||
handAuxValue = itemDict.get("newAuxValue", 0)
|
||||
|
||||
# 检查方块是否有记录且类型匹配
|
||||
if record and record["count"] > 0:
|
||||
# 已有记录 → 检查箭类型是否一致
|
||||
if record.get("itemName") != handItemName or record.get("auxValue", 0) != handAuxValue:
|
||||
return # 类型不匹配,不能混存
|
||||
else:
|
||||
# 无记录 → 新建
|
||||
record = {
|
||||
"count": 0,
|
||||
"itemName": handItemName,
|
||||
"auxValue": handAuxValue,
|
||||
}
|
||||
|
||||
# 计算实际可存数量
|
||||
maxCount = recipe.get("maxCount", 32)
|
||||
if record["count"] >= maxCount:
|
||||
return # 已满
|
||||
storeCount = min(handCount, maxCount - record["count"])
|
||||
|
||||
# 从玩家手中扣减
|
||||
self._consumeItemCount(playerId, itemDict, storeCount)
|
||||
|
||||
# 更新记录
|
||||
record["count"] += storeCount
|
||||
|
||||
# 替换方块
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# ★ 保存记录到新方块
|
||||
# 注意:这里不用延迟写入。虽然 SetBlockNew 替换方块后 block entity
|
||||
# 可能尚未完全初始化,但 GetBlockEntityData 仍然能获取到对象并写入。
|
||||
# 延迟写入反而可能导致退出游戏后数据不持久化。
|
||||
self._saveRecord(key, playerId, pos, dim, record)
|
||||
|
||||
elif arrowMode == "retrieve":
|
||||
# ---- 取箭 ----
|
||||
if not record or record["count"] <= 0:
|
||||
return
|
||||
|
||||
retCount = record["count"]
|
||||
retItemName = record["itemName"]
|
||||
retAuxValue = record.get("auxValue", 0)
|
||||
|
||||
# 给玩家箭
|
||||
self.giveItem(playerId, retItemName, retCount, retAuxValue)
|
||||
|
||||
# 替换方块
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# ★ 清空记录
|
||||
self.clearSwitchRecord(key, playerId, pos, dim)
|
||||
|
||||
def _consumeItemCount(self, playerId, itemDict, amount):
|
||||
"""
|
||||
从玩家主手批量扣减指定数量的物品。
|
||||
如果手中数量刚好等于 amount → 清空主手。
|
||||
如果手中数量大于 amount → 减少 amount。
|
||||
"""
|
||||
if not itemDict or amount <= 0:
|
||||
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)})
|
||||
|
||||
# ============================================================================
|
||||
# 延迟写入帮手(解决 SetBlockNew 替换方块后 block entity 未初始化的问题)
|
||||
# ============================================================================
|
||||
|
||||
def _saveRecordDelayed(self, key, playerId, pos, dim, record):
|
||||
"""
|
||||
延迟一帧保存记录到方块实体数据。
|
||||
|
||||
SetBlockNew 替换方块后,新方块的 block entity 需要引擎内部初始化时间,
|
||||
立即 GetBlockEntityData 可能返回 None 导致写入静默失败。
|
||||
|
||||
先更新内存,再延迟写入方块实体。
|
||||
"""
|
||||
self.switchRecord[key] = record
|
||||
gameComp.AddTimer(0.1, self.saveSwitchRecord, playerId, pos, dim, record)
|
||||
|
||||
def _clearRecordDelayed(self, key, playerId, pos, dim):
|
||||
"""
|
||||
延迟一帧清除方块实体数据。
|
||||
"""
|
||||
self.switchRecord.pop(key, None)
|
||||
gameComp.AddTimer(0.1, self._clearRecordWrite, playerId, pos, dim)
|
||||
|
||||
def _clearRecordWrite(self, playerId, pos, dim):
|
||||
"""
|
||||
实际执行清除写入(被 AddTimer 调用)。
|
||||
"""
|
||||
self._writeBlockData(dim, pos, rtdd_swt="", rtdd_swc=0, rtdd_swi="", rtdd_swa=0, rtdd_sws=[])
|
||||
|
||||
# ============================================================================
|
||||
# 工具方法:获取玩家手中物品
|
||||
# ============================================================================
|
||||
|
||||
def getCarriedItem(self, playerId):
|
||||
"""
|
||||
获取玩家主手(右手)拿的物品。
|
||||
空手返回 None。
|
||||
"""
|
||||
itemComp = comp.CreateItem(playerId)
|
||||
return itemComp.GetPlayerItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
|
||||
|
||||
# ============================================================================
|
||||
# 工具方法:消耗物品
|
||||
# ============================================================================
|
||||
|
||||
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:
|
||||
# 数量够 → 减少 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):
|
||||
"""
|
||||
把指定位置的方块替换成新方块。
|
||||
|
||||
对照 BlockBlock.py 的标准写法:
|
||||
- 使用命名参数
|
||||
- oldBlockHandling=0 表示直接替换(不保留旧方块数据)
|
||||
- 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, auxValue=0):
|
||||
"""
|
||||
往玩家背包里塞一个物品。
|
||||
如果背包满了,物品会掉在地上。
|
||||
"""
|
||||
comp.CreateItem(playerId).SpawnItemToPlayerInv(
|
||||
{"itemName": itemName, "count": count, "auxValue": auxValue}, playerId)
|
||||
+7
-5
@@ -106,15 +106,17 @@ class FeatureDispatcher(BlueFoxServerSystem):
|
||||
def startTimerFor(cls, blockName, dim, pos, playerId=None):
|
||||
"""
|
||||
右键服务调用:把"计时源方块"的计时启动任务转发给对应的计时服务。
|
||||
- 例如:烧烤右键"烤鸡(生)"开始烤制;动物装满饲料盆后自动开始变空计时
|
||||
- 内部通过 timerOwner 路由表 O(1) 找到计时服务,调它的 startTimer
|
||||
返回 True=找到了计时服务并处理;False=该方块不属于任何计时服务
|
||||
- 例如:烧烤右键"烤鸡(生)"开始烤制;动物装满饲料盆后自动开始变空计时;
|
||||
饮料冰棍盒手持冰块右键未定型启动定型计时
|
||||
- 内部通过 timerOwner 路由表 O(1) 找到计时服务,调它的 startTimer,
|
||||
并返回计时服务的实际返回值:
|
||||
True = 本次真正启动了计时(如饮料冰棍盒据此消耗冰块)
|
||||
False = 没有计时服务 / 该方块已在计时中(不重复启动)
|
||||
"""
|
||||
service = cls.timerOwner.get(blockName)
|
||||
if not service:
|
||||
return False
|
||||
service.startTimer(dim, pos, blockName, playerId)
|
||||
return True
|
||||
return service.startTimer(dim, pos, blockName, playerId)
|
||||
|
||||
def __init__(self, namespace, systemName):
|
||||
"""
|
||||
|
||||
+5
-3
@@ -118,28 +118,30 @@ class BarbecueTimerService(BlueFoxServerSystem):
|
||||
启动烤制计时:先检查下方篝火,通过后写 开始时间戳+时长 到方块实体数据。
|
||||
- 启动前检查:下方不是篝火 → 拒绝并提示(烧烤必须右键激活,不会自动烤)
|
||||
- 已在本规则计时中的方块不会重复重置时间(保持原剩余时间)
|
||||
返回 True=找到规则并处理(含已计时中/篝火检查失败);False=不属于本计时功能
|
||||
"""
|
||||
recipe = self.barbecueTimers.get(blockName)
|
||||
if not recipe:
|
||||
return
|
||||
return False
|
||||
|
||||
# 启动前检查下方篝火(烧烤必须下方是篝火才能开始烤制)
|
||||
if not self.checkBelow(recipe, dim, pos):
|
||||
if playerId:
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§c支杆下方需要放一个篝火(营火)才能烤制!")
|
||||
return
|
||||
return True
|
||||
|
||||
# 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间)
|
||||
try:
|
||||
data = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if data and data["rtdd_tid"] == blockName:
|
||||
return
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
self._writeTimerData(dim, pos, recipe, playerId)
|
||||
# 烤制开始提示
|
||||
if playerId and recipe.get("startTip"):
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, recipe["startTip"])
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# 三、tick 检查与到点转换
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
================================================================================
|
||||
乡野饮料(xy_drink)转化规则数据
|
||||
================================================================================
|
||||
所有"乡野饮料"(xy_drink) 分类下的转化规则常量都在这里。
|
||||
只被 YinliaoService / YinliaoTimerService 读取,FeatureDispatcher 通过方块名/物品名
|
||||
集合路由到对应服务,不会去查其他功能的数据库。
|
||||
|
||||
【三大分组】
|
||||
1) 果汁·榨汁饮品(榨汁机系列 juicer / juicer_0~4) —— 全流程右键转换
|
||||
果汁由第一步放入的水果决定并【持久化到方块实体数据】, 退出游戏重进后
|
||||
榨汁完成仍取回正确果汁(详见 YinliaoService._readFruit / _writeFruit)。
|
||||
2) 冰棍(冰棍盒 icelollybox_1/2 + 各水果盒 icelollyboxb~f)
|
||||
未定型 → (手持冰块右键启动) 计时30秒 → 定型 → (手持水果x2) → 各水果盒
|
||||
→ (空手右键) 取出2根冰棍, 还原未定型。
|
||||
冰棍堆 popsiclepile_2/3: 组合式(苹果冰棍+堆 → 堆2, 空手取回 LIFO)。
|
||||
3) 冰棍盒·炒冰盒(frytheicebox / frytheicebox_1 + 各水果炒冰盒)
|
||||
空 → (手持冰块) 放冰 → (手持水果) 炒冰 → (空手右键) 取出冰沙, 还原空。
|
||||
注: 西瓜炒冰盒可做, 但 西瓜冰沙(melonicecreamplace) 是禁用方块, 无取出规则。
|
||||
|
||||
字段说明(与 AnimalData/BarbecueData 一致):
|
||||
水果物品名 → (成品方块, 名称) 的映射表, 服务端按需读取。
|
||||
================================================================================
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# 一、冰块(广义的冰): 原版 冰块/冰砖/蓝冰/霜冰
|
||||
# 冰棍盒定型、炒冰盒放冰都消耗这几种冰中的任意一种
|
||||
# ============================================================================
|
||||
ICE_ITEMS = [
|
||||
"minecraft:ice",
|
||||
"minecraft:packed_ice",
|
||||
"minecraft:blue_ice",
|
||||
"minecraft:frosted_ice",
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# 二、榨汁机(果汁·榨汁饮品)
|
||||
# ============================================================================
|
||||
# 水果物品名 → (果汁方块, 果汁名)
|
||||
# 西瓜=原版西瓜片 / 橙子=rtdd:orange / 苹果=原版苹果 / 桃子=rtdd:peach /
|
||||
# 葡萄=rtdd:grape / 黄豆=rtdd:soybean(豆浆)
|
||||
JUICE_FRUITS = {
|
||||
"minecraft:melon_slice": ("rtdd:fruitjuice_1", "西瓜汁"),
|
||||
"rtdd:orange": ("rtdd:fruitjuice_2", "橙汁"),
|
||||
"minecraft:apple": ("rtdd:fruitjuice_3", "苹果汁"),
|
||||
"rtdd:peach": ("rtdd:fruitjuice_4", "桃子汁"),
|
||||
"rtdd:grape": ("rtdd:fruitjuice_6", "葡萄汁"),
|
||||
"rtdd:soybean": ("rtdd:fruitjuice_7", "豆浆"),
|
||||
}
|
||||
|
||||
# 榨汁机状态链(空→放入水果→榨汁中1→2→3→完成), 索引按步骤顺序
|
||||
JUICER_CHAIN = [
|
||||
"rtdd:juicer", # 0 榨汁机(空)
|
||||
"rtdd:juicer_0", # 1 榨汁机(放入水果)
|
||||
"rtdd:juicer_1", # 2 榨汁机(榨汁中1)
|
||||
"rtdd:juicer_2", # 3 榨汁机(榨汁中2)
|
||||
"rtdd:juicer_3", # 4 榨汁机(榨汁中3)
|
||||
"rtdd:juicer_4", # 5 榨汁机(完成)
|
||||
]
|
||||
|
||||
# 榨汁中逐级右键的提示(juicer_0→1→2→3→4 各阶段)
|
||||
JUICER_ADVANCE_TIPS = {
|
||||
"rtdd:juicer_0": "§a开始榨汁…",
|
||||
"rtdd:juicer_1": "§a榨汁中…",
|
||||
"rtdd:juicer_2": "§a榨汁中…",
|
||||
"rtdd:juicer_3": "§e榨汁完成!",
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 三、冰棍盒(冰棍)
|
||||
# ============================================================================
|
||||
# 冰棍盒(未定型) icelollybox_1 → 冰棍盒(定型) icelollybox_2 的计时规则:
|
||||
# 手持冰块右键启动(消耗1个冰块), 约30秒定型; autoStart=False 不自动开始
|
||||
ICE_LOLLY_TIMER = {
|
||||
"source": "rtdd:icelollybox_1",
|
||||
"target": "rtdd:icelollybox_2",
|
||||
"time": 30,
|
||||
"autoStart": False,
|
||||
"startTip": "§e已放入冰块,冰棍定型中…",
|
||||
"doneTip": "§e冰棍盒里的冰棍定型了!",
|
||||
}
|
||||
|
||||
# 水果物品名 → (各水果冰棍盒, 冰棍方块, 口味名)
|
||||
# 苹果→icelollyboxc / 桃子→icelollyboxd / 橙子→icelollyboxb /
|
||||
# 葡萄→icelollyboxe / 西瓜→icelollyboxf
|
||||
POPSICLE_FRUITS = {
|
||||
"minecraft:apple": ("rtdd:icelollyboxc", "rtdd:apple_popsicleplace", "苹果"),
|
||||
"rtdd:peach": ("rtdd:icelollyboxd", "rtdd:peach_popsicleplace", "桃子"),
|
||||
"rtdd:orange": ("rtdd:icelollyboxb", "rtdd:orange_popsicle_place", "橙子"),
|
||||
"rtdd:grape": ("rtdd:icelollyboxe", "rtdd:grape_popsicleplace", "葡萄"),
|
||||
"minecraft:melon_slice": ("rtdd:icelollyboxf", "rtdd:melon_popsicleplace", "西瓜"),
|
||||
}
|
||||
|
||||
# 冰棍盒方块 → (水果物品名, 冰棍方块, 口味名) 反向表(取冰棍用)
|
||||
POPSICLE_BOX_INFO = {v[0]: (k, v[1], v[2]) for k, v in POPSICLE_FRUITS.items()}
|
||||
|
||||
# 冰棍堆组合:
|
||||
# popsiclepile_2(橙子+西瓜) + 苹果冰棍 → popsiclepile_3(橙子+西瓜+苹果)
|
||||
# popsiclepile_3 空手取回 → 苹果冰棍, 退回 popsiclepile_2
|
||||
# (其他口味组合暂无对应堆方块, 不支持堆叠)
|
||||
POPSICLE_PILE_ADD = "rtdd:apple_popsicleplace" # 能插上堆的冰棍
|
||||
POPSICLE_PILE_2 = "rtdd:popsiclepile_2" # 2根堆(基础)
|
||||
POPSICLE_PILE_3 = "rtdd:popsiclepile_3" # 3根堆
|
||||
|
||||
# ============================================================================
|
||||
# 四、炒冰盒(冰棍盒·炒冰盒 分组)
|
||||
# ============================================================================
|
||||
# 水果物品名 → (各水果炒冰盒, 冰沙方块, 口味名)
|
||||
# 五种口味齐全: 西瓜冰沙(melonicecreamplace) 已从禁用移回乡野饮料, 可正常取出
|
||||
FRYICE_FRUITS = {
|
||||
"minecraft:apple": ("rtdd:appleicebox", "rtdd:appleicecreamplace", "苹果"),
|
||||
"rtdd:grape": ("rtdd:grapeicebox", "rtdd:grapeicecreamplace", "葡萄"),
|
||||
"minecraft:melon_slice": ("rtdd:melonicebox", "rtdd:melonicecreamplace", "西瓜"),
|
||||
"rtdd:orange": ("rtdd:orangeicebox", "rtdd:orangeicecreamplace", "橘子"),
|
||||
"rtdd:peach": ("rtdd:peachicebox", "rtdd:peachicecreamplace", "桃子"),
|
||||
}
|
||||
|
||||
# 炒冰盒方块 → (水果物品名, 冰沙方块, 口味名) 反向表(取冰沙用)
|
||||
FRYICE_BOX_INFO = {v[0]: (k, v[1], v[2]) for k, v in FRYICE_FRUITS.items()}
|
||||
Whitespace-only changes.
+20
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
乡野饮料客户端(预留)
|
||||
=====================
|
||||
目前乡野饮料功能的服务端逻辑已由 YinliaoService + YinliaoTimerService 处理,
|
||||
客户端暂不需要额外的 UI 工作。
|
||||
如果以后需要榨汁进度提示、冰棍定型倒计时等 UI,可以在这里扩展。
|
||||
"""
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
ClientSystem = clientApi.GetClientSystemCls()
|
||||
|
||||
|
||||
class YinliaoClient(ClientSystem):
|
||||
def __init__(self, namespace, systemName):
|
||||
ClientSystem.__init__(self, namespace, systemName)
|
||||
print "===== YinliaoClient init ====="
|
||||
|
||||
def Destroy(self):
|
||||
print "===== YinliaoClient Destroy ====="
|
||||
Whitespace-only changes.
+449
@@ -0,0 +1,449 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
================================================================================
|
||||
乡野饮料·右键转换引擎(服务端)
|
||||
================================================================================
|
||||
处理"乡野饮料"(xy_drink) 分类下的全部【右键切换型】转换:
|
||||
1) 榨汁机(果汁·榨汁饮品): 全流程右键
|
||||
空(juicer) + 水果 → 放入水果(juicer_0, 记录水果类型)
|
||||
→ 榨汁中1/2/3(juicer_1/2/3) → 完成(juicer_4)
|
||||
→ 空手右键取出对应果汁, 还原空
|
||||
【水果类型持久化】放入水果时把水果写进方块实体数据(rtdd_jf), 之后每次
|
||||
换态(SetBlockNew 会清空方块实体)都【先读旧→替换→写回新】, 因此玩家
|
||||
在 榨汁2→榨汁3 过程中退出游戏, 重进后取出的果汁仍是第一步放入的水果。
|
||||
2) 冰棍盒(冰棍):
|
||||
未定型(icelollybox_1) + 冰块 → 启动30秒定型计时(转发给计时服务, 消耗1冰)
|
||||
定型(icelollybox_2) + 水果x2 → 各水果冰棍盒(icelollyboxb~f)
|
||||
各水果冰棍盒 空手右键 → 取出2根冰棍, 还原未定型
|
||||
3) 炒冰盒(冰棍盒·炒冰盒):
|
||||
空(frytheicebox) + 冰块 → 放冰(frytheicebox_1)
|
||||
放冰 + 水果 → 各水果炒冰盒(appleicebox 等)
|
||||
各水果炒冰盒(除西瓜) 空手右键 → 取出1个冰沙, 还原空
|
||||
4) 冰棍堆(组合, 同厨具架逻辑):
|
||||
popsiclepile_2(橙子+西瓜) + 苹果冰棍 → popsiclepile_3(橙子+西瓜+苹果)
|
||||
popsiclepile_3 空手右键 → 取出苹果冰棍(LIFO), 退回 popsiclepile_2
|
||||
|
||||
【与计时转换解耦】
|
||||
冰棍盒"未定型→定型"的 30 秒计时拆到独立类 YinliaoTimerService。
|
||||
本类右键到计时源方块(icelollybox_1 + 冰块)时, 调 FeatureDispatcher.startTimerFor
|
||||
转发给计时服务启动(冰块消耗由本类处理)。
|
||||
|
||||
【事件监听在哪?】
|
||||
本类【不】监听任何事件!
|
||||
所有事件由 FeatureDispatcher 统一监听, 按 blockOwner 路由到本类 handleBlockUse。
|
||||
|
||||
【本类怎么做(自上而下的方法系列)】
|
||||
handleBlockUse ← 入口:分发器转发右键方块事件
|
||||
├─ _handleJuicer ← 榨汁机: 放入水果/逐级榨汁/取出果汁
|
||||
│ ├─ _readFruit / _writeFruit ← 水果类型 读/写(持久化)
|
||||
├─ _handleLollyBox ← 冰棍盒: 加冰块(启动计时)/加水果/取冰棍
|
||||
├─ _handleFryIceBox ← 炒冰盒: 加冰块/加水果/取冰沙
|
||||
└─ _handlePopsiclePile ← 冰棍堆: 插苹果冰棍/取回
|
||||
(通用工具) getCarriedItem / consumeItem / replaceBlock / dropItems / 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
|
||||
|
||||
# ---- 方块实体数据组件(记录榨汁机水果类型; SetBlockNew 后带延迟兜底) ----
|
||||
blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
|
||||
# ---- 游戏组件(AddTimer 延迟写入 block entity 兜底) ----
|
||||
gameComp = comp.CreateGame(LevelId)
|
||||
|
||||
# ---- 只导入乡野饮料自己的数据库 ----
|
||||
from ..YinliaoData import (
|
||||
ICE_ITEMS,
|
||||
JUICE_FRUITS, JUICER_CHAIN, JUICER_ADVANCE_TIPS,
|
||||
ICE_LOLLY_TIMER, POPSICLE_FRUITS, POPSICLE_BOX_INFO,
|
||||
POPSICLE_PILE_ADD, POPSICLE_PILE_2, POPSICLE_PILE_3,
|
||||
FRYICE_FRUITS, FRYICE_BOX_INFO,
|
||||
)
|
||||
|
||||
# ---- 统一分发器 ----
|
||||
from ...service.FeatureDispatcher import FeatureDispatcher
|
||||
|
||||
|
||||
class YinliaoService(BlueFoxServerSystem):
|
||||
"""
|
||||
乡野饮料·右键转换引擎
|
||||
|
||||
【核心数据结构】
|
||||
featureBlocks = {方块名, ...} # 本功能管辖的所有右键方块(注册路由用)
|
||||
cooldown = {key: 时间戳} # 连点冷却表
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, systemName):
|
||||
super(YinliaoService, self).__init__(namespace, systemName)
|
||||
print "===== YinliaoService init ====="
|
||||
|
||||
# ---- 收集本功能管辖的所有右键方块 ----
|
||||
self.featureBlocks = set()
|
||||
for b in JUICER_CHAIN: # 榨汁机全态
|
||||
self.featureBlocks.add(b)
|
||||
self.featureBlocks.add("rtdd:icelollybox_1") # 冰棍盒(未定型/定型)
|
||||
self.featureBlocks.add("rtdd:icelollybox_2")
|
||||
for _f, info in POPSICLE_FRUITS.items(): # 各水果冰棍盒
|
||||
self.featureBlocks.add(info[0])
|
||||
self.featureBlocks.add("rtdd:frytheicebox") # 炒冰盒(空/放冰)
|
||||
self.featureBlocks.add("rtdd:frytheicebox_1")
|
||||
for _f, info in FRYICE_FRUITS.items(): # 各水果炒冰盒
|
||||
self.featureBlocks.add(info[0])
|
||||
self.featureBlocks.add(POPSICLE_PILE_2) # 冰棍堆
|
||||
self.featureBlocks.add(POPSICLE_PILE_3)
|
||||
|
||||
# ---- 右键冷却表 ----
|
||||
self.cooldown = {}
|
||||
|
||||
# ---- 注册到统一分发器 ----
|
||||
FeatureDispatcher.register(self)
|
||||
|
||||
# ============================================================================
|
||||
# 一、给 FeatureDispatcher 的接口
|
||||
# ============================================================================
|
||||
|
||||
def getFeatureBlocks(self):
|
||||
"""返回本功能管辖的所有方块名集合(右键路由用)"""
|
||||
return self.featureBlocks
|
||||
|
||||
def getItemUseOnRules(self):
|
||||
"""手持水果右键方块表面不需要单独路由, 返回空。
|
||||
水果为可食用物品, 右键饮料方块时会走 ServerBlockUseEvent(与挂鱼一致),
|
||||
由分发器取消后续"吃"行为。"""
|
||||
return {}
|
||||
|
||||
def handleBlockUse(self, args):
|
||||
"""
|
||||
由 FeatureDispatcher 转发的右键方块入口。
|
||||
返回 True = 本功能处理了这个右键(分发器会取消默认行为/吃食物)
|
||||
返回 False = 当前组合没有匹配规则(交还默认行为)
|
||||
"""
|
||||
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 JUICER_CHAIN:
|
||||
return self._handleJuicer(blockName, playerId, pos, dim, itemDict, itemName, key)
|
||||
# ---- 冰棍盒 ----
|
||||
if blockName == "rtdd:icelollybox_1" or blockName == "rtdd:icelollybox_2" \
|
||||
or blockName in POPSICLE_BOX_INFO:
|
||||
return self._handleLollyBox(blockName, playerId, pos, dim, itemDict, itemName, key)
|
||||
# ---- 炒冰盒 ----
|
||||
if blockName == "rtdd:frytheicebox" or blockName == "rtdd:frytheicebox_1" \
|
||||
or blockName in FRYICE_BOX_INFO:
|
||||
return self._handleFryIceBox(blockName, playerId, pos, dim, itemDict, itemName, key)
|
||||
# ---- 冰棍堆 ----
|
||||
if blockName == POPSICLE_PILE_2 or blockName == POPSICLE_PILE_3:
|
||||
return self._handlePopsiclePile(blockName, playerId, pos, dim, itemDict, itemName, key)
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# 二、榨汁机(全流程右键, 水果类型持久化)
|
||||
# ============================================================================
|
||||
|
||||
def _handleJuicer(self, blockName, playerId, pos, dim, itemDict, itemName, key):
|
||||
"""榨汁机分四步: 放入水果 → 逐级榨汁 → 空手取出果汁"""
|
||||
# ---- 空榨汁机 + 水果 → 放入水果(记录水果类型) ----
|
||||
if blockName == "rtdd:juicer":
|
||||
juiceInfo = JUICE_FRUITS.get(itemName)
|
||||
if not juiceInfo:
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
self.consumeItem(playerId, itemDict, 1)
|
||||
self.replaceBlock(pos, dim, "rtdd:juicer_0", playerId)
|
||||
# 替换后写水果类型(block entity 可能未初始化, 带延迟兜底)
|
||||
self._writeFruit(dim, pos, itemName)
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a已放入水果,右键开始榨汁")
|
||||
return True
|
||||
|
||||
# ---- 榨汁中逐级推进(juicer_0→1→2→3→4): 任意右键, 换态并保留水果类型 ----
|
||||
if blockName in JUICER_ADVANCE_TIPS:
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
# 先读旧方块的水果类型(SetBlockNew 会清空方块实体)
|
||||
fruit = self._readFruit(pos, dim)
|
||||
idx = JUICER_CHAIN.index(blockName)
|
||||
self.replaceBlock(pos, dim, JUICER_CHAIN[idx + 1], playerId)
|
||||
if fruit:
|
||||
self._writeFruit(dim, pos, fruit) # 替换后写回新方块
|
||||
tip = JUICER_ADVANCE_TIPS.get(blockName)
|
||||
if tip:
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, tip)
|
||||
return True
|
||||
|
||||
# ---- 完成(juicer_4) 空手右键 → 取出果汁, 还原空 ----
|
||||
if blockName == "rtdd:juicer_4":
|
||||
if itemName and itemName != "minecraft:air":
|
||||
return False # 必须空手取出, 手持其他物品不处理
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
fruit = self._readFruit(pos, dim)
|
||||
juiceInfo = JUICE_FRUITS.get(fruit) if fruit else None
|
||||
if not juiceInfo:
|
||||
return False # 无水果记录(创造直接放置的完成态) → 不反应
|
||||
juiceBlock, juiceName = juiceInfo
|
||||
self.dropItems(dim, pos, [{"name": juiceBlock, "count": 1}])
|
||||
self.replaceBlock(pos, dim, "rtdd:juicer", playerId) # 还原空, 记录随方块清除
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取出了" + juiceName)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# 三、冰棍盒(加冰块启动定型计时 / 加水果 / 取冰棍)
|
||||
# ============================================================================
|
||||
|
||||
def _handleLollyBox(self, blockName, playerId, pos, dim, itemDict, itemName, key):
|
||||
# ---- 未定型 + 冰块 → 启动30秒定型计时(消耗1块冰) ----
|
||||
if blockName == "rtdd:icelollybox_1":
|
||||
if itemName not in ICE_ITEMS:
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
# 转发给计时服务: True=本次启动, False=已在定型中(不重复消耗冰块)
|
||||
started = FeatureDispatcher.startTimerFor(blockName, dim, pos, playerId)
|
||||
if not started:
|
||||
return True # 已在定型中, 拦截但不消耗
|
||||
self.consumeItem(playerId, itemDict, 1)
|
||||
if ICE_LOLLY_TIMER.get("startTip"):
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, ICE_LOLLY_TIMER["startTip"])
|
||||
return True
|
||||
|
||||
# ---- 定型 + 水果x2 → 各水果冰棍盒 ----
|
||||
if blockName == "rtdd:icelollybox_2":
|
||||
info = POPSICLE_FRUITS.get(itemName)
|
||||
if not info:
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
# 数量检查: 需要2个水果, 不足则红字提示且不转换
|
||||
if (itemDict.get("count") or 0) < 2:
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(
|
||||
playerId, "§c需要2个" + info[2] + "才能做冰棍")
|
||||
return True
|
||||
boxBlock, _popsicle, flavor = info
|
||||
self.consumeItem(playerId, itemDict, 2)
|
||||
self.replaceBlock(pos, dim, boxBlock, playerId)
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a" + flavor + "冰棍做好了!")
|
||||
return True
|
||||
|
||||
# ---- 各水果冰棍盒 空手右键 → 取出2根冰棍, 还原未定型 ----
|
||||
info = POPSICLE_BOX_INFO.get(blockName)
|
||||
if not info:
|
||||
return False
|
||||
if itemName and itemName != "minecraft:air":
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
_fruit, popsicle, flavor = info
|
||||
self.dropItems(dim, pos, [{"name": popsicle, "count": 2}])
|
||||
self.replaceBlock(pos, dim, "rtdd:icelollybox_1", playerId) # 还原未定型
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取出了2根" + flavor + "冰棍")
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# 四、炒冰盒(加冰块 / 加水果 / 取冰沙)
|
||||
# ============================================================================
|
||||
|
||||
def _handleFryIceBox(self, blockName, playerId, pos, dim, itemDict, itemName, key):
|
||||
# ---- 空 + 冰块 → 放冰 ----
|
||||
if blockName == "rtdd:frytheicebox":
|
||||
if itemName not in ICE_ITEMS:
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
self.consumeItem(playerId, itemDict, 1)
|
||||
self.replaceBlock(pos, dim, "rtdd:frytheicebox_1", playerId)
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a已放入冰块,右键放入水果炒冰")
|
||||
return True
|
||||
|
||||
# ---- 放冰 + 水果 → 各水果炒冰盒 ----
|
||||
if blockName == "rtdd:frytheicebox_1":
|
||||
info = FRYICE_FRUITS.get(itemName)
|
||||
if not info:
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
iceBox, _icecream, flavor = info
|
||||
self.consumeItem(playerId, itemDict, 1)
|
||||
self.replaceBlock(pos, dim, iceBox, playerId)
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a" + flavor + "炒冰做好了!")
|
||||
return True
|
||||
|
||||
# ---- 各水果炒冰盒 空手右键 → 取出1个冰沙, 还原空 ----
|
||||
info = FRYICE_BOX_INFO.get(blockName)
|
||||
if not info:
|
||||
return False
|
||||
_fruit, icecream, flavor = info
|
||||
if not icecream:
|
||||
return False # 该口味无成品冰沙(防御), 正常五种口味均有
|
||||
if itemName and itemName != "minecraft:air":
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
self.dropItems(dim, pos, [{"name": icecream, "count": 1}])
|
||||
self.replaceBlock(pos, dim, "rtdd:frytheicebox", playerId) # 还原空
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取出了" + flavor + "冰沙")
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# 五、冰棍堆(组合式: 插苹果冰棍 / 空手取回 LIFO)
|
||||
# ============================================================================
|
||||
|
||||
def _handlePopsiclePile(self, blockName, playerId, pos, dim, itemDict, itemName, key):
|
||||
# ---- 堆(橙子+西瓜) + 苹果冰棍 → 堆2(橙子+西瓜+苹果) ----
|
||||
if blockName == POPSICLE_PILE_2:
|
||||
if itemName != POPSICLE_PILE_ADD:
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
self.consumeItem(playerId, itemDict, 1)
|
||||
self.replaceBlock(pos, dim, POPSICLE_PILE_3, playerId)
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a把苹果冰棍插到冰棍堆上")
|
||||
return True
|
||||
|
||||
# ---- 堆2 空手右键 → 取出苹果冰棍(后放先取), 退回堆 ----
|
||||
if blockName == POPSICLE_PILE_3:
|
||||
if itemName and itemName != "minecraft:air":
|
||||
return False
|
||||
if not self.checkCooldown(key, 0.5):
|
||||
return True
|
||||
self.dropItems(dim, pos, [{"name": POPSICLE_PILE_ADD, "count": 1}])
|
||||
self.replaceBlock(pos, dim, POPSICLE_PILE_2, playerId)
|
||||
comp.CreateMsg(playerId).NotifyOneMessage(playerId, "§a取走了苹果冰棍")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# ============================================================================
|
||||
# 六、水果类型 读/写(榨汁机果汁持久化, 字段 rtdd_jf)
|
||||
# ============================================================================
|
||||
|
||||
def _writeFruit(self, dim, pos, itemName):
|
||||
"""把水果类型写入榨汁机方块实体数据(替换方块后调用, 带 0.1s 延迟兜底)"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
gameComp.AddTimer(0.1, self._writeFruitDelayed, dim, pos, itemName)
|
||||
return
|
||||
blockData["rtdd_jf"] = str(itemName)
|
||||
except Exception:
|
||||
gameComp.AddTimer(0.1, self._writeFruitDelayed, dim, pos, itemName)
|
||||
|
||||
def _writeFruitDelayed(self, dim, pos, itemName):
|
||||
"""延迟写入兜底(block entity 未初始化时)"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is not None:
|
||||
blockData["rtdd_jf"] = str(itemName)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _readFruit(self, pos, dim):
|
||||
"""从方块实体数据读出水果类型, 无记录返回 None"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
return None
|
||||
return blockData["rtdd_jf"] or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# 七、工具方法:获取玩家主手物品
|
||||
# ============================================================================
|
||||
|
||||
def getCarriedItem(self, playerId):
|
||||
itemComp = comp.CreateItem(playerId)
|
||||
return itemComp.GetPlayerItem(
|
||||
serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0, True)
|
||||
|
||||
# ============================================================================
|
||||
# 八、工具方法:消耗物品
|
||||
# ============================================================================
|
||||
|
||||
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 replaceBlock(self, pos, dim, newBlockName, playerId=None):
|
||||
"""
|
||||
替换指定位置的方块。
|
||||
playerId 可为 None, 用 LevelId 创建组件。
|
||||
"""
|
||||
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 dropItems(self, dim, pos, dropList):
|
||||
"""
|
||||
dropList 格式:[{"name": "xxx", "count": N}, ...]
|
||||
在方块上方生成掉落物实体, 让它自然掉到地上。
|
||||
"""
|
||||
for d in dropList:
|
||||
itemDict = {
|
||||
"itemName": d["name"],
|
||||
"count": d.get("count", 1),
|
||||
"auxValue": d.get("aux", 0),
|
||||
}
|
||||
self.CreateEngineItemEntity(itemDict, dim, (pos[0], pos[1] + 0.5, pos[2]))
|
||||
|
||||
# ============================================================================
|
||||
# 十一、工具方法:冷却检查
|
||||
# ============================================================================
|
||||
|
||||
def checkCooldown(self, key, cd=0.5):
|
||||
"""
|
||||
右键冷却:同一方块 cd 秒内只响应一次,防止连点刷物品/刷提示。
|
||||
"""
|
||||
now = time.time()
|
||||
last = self.cooldown.get(key, 0)
|
||||
if now - last < cd:
|
||||
return False
|
||||
self.cooldown[key] = now
|
||||
return True
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
================================================================================
|
||||
乡野饮料·计时转换引擎(服务端)
|
||||
================================================================================
|
||||
只处理"乡野饮料"(xy_drink) 分类下的【计时自动转换】:
|
||||
冰棍盒(未定型 icelollybox_1) → 冰棍盒(定型 icelollybox_2) 约30秒
|
||||
|
||||
【与右键转换解耦】
|
||||
同分类的"右键切换"(榨汁/冰棍盒加水果/炒冰盒/冰棍堆)在 YinliaoService。
|
||||
本类只负责计时,两者通过 FeatureDispatcher 协作:
|
||||
- YinliaoService 手持冰块右键 未定型 → 调 FeatureDispatcher.startTimerFor
|
||||
转发到本类的 startTimer 启动定型计时(冰块消耗由右键服务处理)
|
||||
- 服务器 tick(ServerBlockEntityTickEvent)由 FeatureDispatcher 按 timerOwner
|
||||
路由到本类的 handleBlockEntityTick,到点即转换为 定型
|
||||
|
||||
【计时怎么做到"退出游戏继续计时"】
|
||||
不做倒计时,只记"开始时刻":
|
||||
- 启动时把 开始时间戳(rtdd_tstart)+总时长(rtdd_tdur) 写进方块实体数据
|
||||
- 每帧 tick 判断 当前时间-开始时刻 >= 总时长 → 到点转换
|
||||
- 时间戳是绝对时间(time.time()),服务器重启/玩家离线,真实时间仍在流逝,
|
||||
回来后 tick 直接算差值,剩余时间自然准确
|
||||
|
||||
【事件监听在哪?】
|
||||
本类【不】监听任何事件!
|
||||
ServerBlockEntityTickEvent 由 FeatureDispatcher 统一监听,再按 timerOwner
|
||||
路由到本类的 handleBlockEntityTick。
|
||||
|
||||
【本类怎么做(自上而下的方法系列)】
|
||||
handleBlockEntityTick ← 入口:分发器转发 tick 事件
|
||||
└─ _tickProcess ← 判断是否到点 / autoStart=False 不自动补写
|
||||
└─ _finishTimer ← 到点执行转换 + 完成提示
|
||||
startTimer ← 入口:YinliaoService 转发过来启动定型
|
||||
└─ _writeTimerData ← 写 开始时刻+时长+启动者 到方块实体
|
||||
_clearTimerData ← 清理方块上的计时数据
|
||||
================================================================================
|
||||
"""
|
||||
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 ..YinliaoData import ICE_LOLLY_TIMER
|
||||
|
||||
# ---- 统一分发器(本类把自己注册进计时路由表,让分发器把 tick 事件转发过来) ----
|
||||
from ...service.FeatureDispatcher import FeatureDispatcher
|
||||
|
||||
|
||||
class YinliaoTimerService(BlueFoxServerSystem):
|
||||
"""
|
||||
乡野饮料·计时转换引擎
|
||||
|
||||
【核心数据结构】
|
||||
iceLollyTimers = {计时源方块名: 规则dict} # 目前只有 冰棍盒未定型
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, systemName):
|
||||
super(YinliaoTimerService, self).__init__(namespace, systemName)
|
||||
print "===== YinliaoTimerService init ====="
|
||||
|
||||
# ---- 收集计时型规则(source → 规则) ----
|
||||
self.iceLollyTimers = {ICE_LOLLY_TIMER["source"]: ICE_LOLLY_TIMER}
|
||||
|
||||
# ---- 注册到分发器的计时路由表(tick 事件按方块名路由到这里) ----
|
||||
FeatureDispatcher.registerTimer(self)
|
||||
|
||||
# ============================================================================
|
||||
# 一、给 FeatureDispatcher 的接口
|
||||
# ============================================================================
|
||||
|
||||
def getTimerBlocks(self):
|
||||
"""返回本功能的所有"计时源方块"集合(tick 路由用)"""
|
||||
return set(self.iceLollyTimers.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)
|
||||
|
||||
# ============================================================================
|
||||
# 二、定型计时启动(YinliaoService 手持冰块右键未定型后转发调用)
|
||||
# ============================================================================
|
||||
|
||||
def startTimer(self, dim, pos, blockName, playerId=None):
|
||||
"""
|
||||
启动定型计时:把 开始时间戳+时长 写入方块实体数据。
|
||||
返回 True = 本次真正启动(右键服务据此消耗冰块)
|
||||
返回 False = 已在定型中 / 无规则(不重复消耗)
|
||||
"""
|
||||
recipe = self.iceLollyTimers.get(blockName)
|
||||
if not recipe:
|
||||
return False
|
||||
# 去重:如果该方块已经在本规则计时中,不重置(保持原剩余时间)
|
||||
try:
|
||||
data = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if data and data["rtdd_tid"] == blockName:
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
self._writeTimerData(dim, pos, recipe, playerId)
|
||||
return True
|
||||
|
||||
# ============================================================================
|
||||
# 三、tick 检查与到点转换
|
||||
# ============================================================================
|
||||
|
||||
def _tickProcess(self, dim, pos, blockName):
|
||||
"""
|
||||
tick 主逻辑:
|
||||
1) 按事件自带的当前方块名判断是否计时源
|
||||
2) 是计时源但没有计时数据 → 只有 autoStart=True 才自动补写。
|
||||
冰棍盒定型 autoStart=False:必须右键(放冰块)激活, 这里不补写
|
||||
3) 有计时数据 → 到点执行转换(重启后时间戳照常生效)
|
||||
注意:GetBlockEntityData 返回的对象不支持 .get(),统一用 [] 读取。
|
||||
"""
|
||||
try:
|
||||
recipe = self.iceLollyTimers.get(blockName)
|
||||
if not recipe:
|
||||
return # 当前方块不是计时源
|
||||
|
||||
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
|
||||
# 替换方块为定型
|
||||
self.replaceBlock(pos, dim, recipe["target"], None)
|
||||
self._clearTimerData(dim, pos)
|
||||
if recipe.get("doneTip"):
|
||||
self._notifyIfOnline(data["rtdd_tp"], recipe["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 replaceBlock(self, pos, dim, newBlockName, playerId=None):
|
||||
"""替换指定位置的方块(计时回调时没有玩家, 用 LevelId 创建组件)"""
|
||||
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)
|
||||
Whitespace-only changes.
Reference in new issue
Block a user