工具切换,食谱切换 区分 减少合并占用内存,箭桶有bug
This commit is contained in:
1 parent
737b3f09ea
commit
e8a22d8cb3
4 files changed
+616
-269
No files matched your search
+564
@@ -0,0 +1,564 @@
|
||||
# -*- 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)
|
||||
|
||||
# ---- 导入工具方块切换规则 ----
|
||||
# BLOCK_SWITCHES 是一个列表,每条规则是一个 dict,格式见 BlockSwitchData.py 顶部注释
|
||||
from ..BlockSwitchData import BLOCK_SWITCHES
|
||||
|
||||
|
||||
# ================================================================================
|
||||
# 工具函数:读取物品 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 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) —— 空手也能触发的规则
|
||||
|
||||
返回:匹配到的规则 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:
|
||||
return recipe
|
||||
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# 工具方块切换处理
|
||||
# ============================================================================
|
||||
|
||||
def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key, blockName):
|
||||
"""
|
||||
处理工具方块的切换逻辑(箭桶、挂肉钩、水槽、盘子架、药水架、书柜等)。
|
||||
|
||||
支持三种模式:
|
||||
1. recordType="count" → 记录一种物品+数量(如箭桶存箭)
|
||||
2. recordType="stack" → 后进先出堆栈(如药水架、书柜)
|
||||
3. 无 record → 纯切换,不记录(如挂肉钩直接换状态)
|
||||
|
||||
流程:
|
||||
1. 如果是计时型(type="timer"),直接替换方块,不消耗物品
|
||||
2. 如果是切换型(type="switch"),根据 recordType 决定记录方式
|
||||
3. 消耗物品、替换方块、更新记录(记录写入方块实体数据,退出重进不丢)
|
||||
"""
|
||||
# ---- 如果规则指定了 tool,检查玩家手里是不是拿着指定工具 ----
|
||||
if recipe.get("tool"):
|
||||
if not itemDict:
|
||||
return
|
||||
carriedItem = itemDict.get("newItemName", "")
|
||||
# 简单判断:物品名是否包含工具名
|
||||
if recipe["tool"] not in carriedItem:
|
||||
return
|
||||
|
||||
# ---- 冷却检查:防止连点右键瞬间切换多次 ----
|
||||
now = time.time()
|
||||
lastOp = self.switchCooldown.get(key, 0)
|
||||
cd = recipe.get("switchCd", 0.5)
|
||||
if now - lastOp < cd:
|
||||
return
|
||||
self.switchCooldown[key] = now
|
||||
|
||||
# ---- 计时型:直接替换方块,不消耗物品,不记录 ----
|
||||
if recipe.get("type") == "timer":
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
return
|
||||
|
||||
# ---- 切换型 ----
|
||||
# 内存中已有数据(_loadBlockDataToMemory 已加载),直接用
|
||||
record = self.switchRecord.get(key)
|
||||
|
||||
# ---- 正向操作:往方块里放东西(source == 被右键的方块) ----
|
||||
if recipe["source"] == blockName and recipe.get("item") is not None:
|
||||
# 玩家手里有物品,往方块里放
|
||||
needSave = False # 标记是否需要保存记录到方块
|
||||
if recipe.get("record"):
|
||||
if recipe.get("recordType") == "count":
|
||||
# 数量记录型:只记录一种物品的总数
|
||||
if not record:
|
||||
record = {
|
||||
"count": 0,
|
||||
"itemName": itemDict["newItemName"],
|
||||
}
|
||||
# 检查是否满了
|
||||
maxCount = recipe.get("maxCount", 999)
|
||||
if record["count"] >= maxCount:
|
||||
return
|
||||
# 记录物品+1
|
||||
record["count"] += 1
|
||||
needSave = True
|
||||
|
||||
elif recipe.get("recordType") == "stack":
|
||||
# 堆栈记录型:后进先出,记录每种物品的完整信息
|
||||
if not record:
|
||||
record = {"stack": []}
|
||||
maxCount = recipe.get("maxCount", 999)
|
||||
if len(record["stack"]) >= maxCount:
|
||||
return
|
||||
# 把物品信息压入堆栈
|
||||
record["stack"].append({
|
||||
"itemName": itemDict["newItemName"],
|
||||
"count": itemDict.get("count", 1),
|
||||
"auxValue": itemDict.get("newAuxValue", 0),
|
||||
"userData": itemDict.get("userData", {}),
|
||||
})
|
||||
needSave = True
|
||||
|
||||
# 消耗物品
|
||||
if recipe.get("consume", 1) > 0:
|
||||
self.consumeItem(playerId, itemDict, recipe.get("consume", 1))
|
||||
|
||||
# 替换方块(旧方块销毁,新方块出现)
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# ★ 关键:替换完方块后再保存记录到新方块上,否则数据随旧方块一起销毁
|
||||
if needSave:
|
||||
self._saveRecord(key, playerId, pos, dim, record)
|
||||
|
||||
# 如果规则指定了给物品(如烧烤架浇水给桶)
|
||||
if recipe.get("giveItem"):
|
||||
self.giveItem(playerId, recipe["giveItem"])
|
||||
|
||||
return
|
||||
|
||||
# ---- 逆向操作:从方块里取东西(空手右键或取回物品) ----
|
||||
if recipe["source"] == blockName and recipe.get("item") is None:
|
||||
needSaveRecord = False # 是否需要保存记录
|
||||
needClearRecord = False # 是否需要清除记录
|
||||
|
||||
if recipe.get("record") and record:
|
||||
if recipe.get("recordType") == "count":
|
||||
# 数量记录型取回:给玩家对应的物品,数量-1
|
||||
if record["count"] > 0:
|
||||
self.giveItem(playerId, record["itemName"])
|
||||
record["count"] -= 1
|
||||
if record["count"] <= 0:
|
||||
needClearRecord = True
|
||||
else:
|
||||
needSaveRecord = True
|
||||
|
||||
elif recipe.get("recordType") == "stack":
|
||||
# 堆栈记录型取回:后进先出,取最后一个放的物品
|
||||
if record["stack"]:
|
||||
topItem = record["stack"].pop()
|
||||
comp.CreateItem(playerId).SpawnItemToPlayerInv(
|
||||
{"itemName": topItem["itemName"],
|
||||
"count": topItem["count"],
|
||||
"auxValue": topItem["auxValue"],
|
||||
"userData": topItem.get("userData", {})},
|
||||
playerId)
|
||||
if not record["stack"]:
|
||||
needClearRecord = True
|
||||
else:
|
||||
needSaveRecord = True
|
||||
|
||||
# 如果规则直接指定了给什么物品(水槽、盘子架取回、挂肉钩退回等)
|
||||
if recipe.get("giveItem"):
|
||||
self.giveItem(playerId, recipe["giveItem"])
|
||||
|
||||
# 替换方块(旧方块销毁,新方块出现)
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# ★ 关键:替换完方块后再保存/清除记录,否则数据随旧方块一起销毁
|
||||
if needClearRecord:
|
||||
self.clearSwitchRecord(key, playerId, pos, dim)
|
||||
elif needSaveRecord:
|
||||
self._saveRecord(key, playerId, pos, dim, record)
|
||||
|
||||
return
|
||||
|
||||
# ============================================================================
|
||||
# 通用持久化帮手(避免重复的 try/except/Get/Set 写法)
|
||||
# ============================================================================
|
||||
|
||||
def _writeBlockData(self, dim, pos, **kwargs):
|
||||
"""
|
||||
通用方块实体数据写入。
|
||||
|
||||
GetBlockEntityData 返回的 BlockEntityData 对象支持 [] 操作,
|
||||
直接赋值即可持久化。官方没有 SetBlockEntityData 方法。
|
||||
"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
if blockData is None:
|
||||
print "[BlockSwitchService] _writeBlockData blockData is None: pos=%s dim=%s" % (pos, dim)
|
||||
return
|
||||
for key, value in kwargs.items():
|
||||
blockData[key] = value
|
||||
except Exception as e:
|
||||
print "[BlockSwitchService] _writeBlockData 写入失败: pos=%s dim=%s err=%s" % (pos, dim, str(e))
|
||||
|
||||
def _saveRecord(self, key, playerId, pos, dim, record):
|
||||
"""
|
||||
同时更新内存和方块实体数据中的工具方块记录。
|
||||
|
||||
替代原本重复出现的:
|
||||
self.switchRecord[key] = record
|
||||
self.saveSwitchRecord(playerId, pos, dim, record)
|
||||
"""
|
||||
self.switchRecord[key] = record
|
||||
self.saveSwitchRecord(playerId, pos, dim, record)
|
||||
|
||||
# ============================================================================
|
||||
# 统一数据加载入口(玩家右键工具方块时先把方块数据恢复到内存)
|
||||
# ============================================================================
|
||||
|
||||
def _loadBlockDataToMemory(self, key, playerId, pos, dim):
|
||||
"""
|
||||
在确定玩家右键命中了一个工具切换规则后,立刻把方块里存的记录加载到内存。
|
||||
|
||||
这样 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", ""))
|
||||
elif "stack" in record:
|
||||
self._writeBlockData(dim, pos,
|
||||
rtdd_swt="stack",
|
||||
rtdd_sws=record["stack"])
|
||||
|
||||
def loadSwitchRecord(self, playerId, pos, dim):
|
||||
"""
|
||||
从方块实体数据里读取工具方块的记录数据。
|
||||
|
||||
返回:
|
||||
有数据 → {"count": N, "itemName": "xxx"} 或 {"stack": [...]}
|
||||
没有 → None
|
||||
"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
swt = blockData["rtdd_swt"] # 记录类型:"count" 或 "stack"
|
||||
if swt == "count":
|
||||
return {
|
||||
"count": blockData["rtdd_swc"],
|
||||
"itemName": blockData["rtdd_swi"],
|
||||
}
|
||||
elif swt == "stack":
|
||||
return {
|
||||
"stack": blockData["rtdd_sws"],
|
||||
}
|
||||
except:
|
||||
return None
|
||||
|
||||
def clearSwitchRecord(self, key, playerId, pos, dim):
|
||||
"""
|
||||
清除工具方块的记录数据(内存+方块实体数据)。
|
||||
"""
|
||||
self.switchRecord.pop(key, None)
|
||||
self._writeBlockData(dim, pos, rtdd_swt="", rtdd_swc=0, rtdd_swi="", rtdd_sws=[])
|
||||
|
||||
# ============================================================================
|
||||
# 工具方法:获取玩家手中物品
|
||||
# ============================================================================
|
||||
|
||||
def 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):
|
||||
"""
|
||||
往玩家背包里塞一个物品。
|
||||
如果背包满了,物品会掉在地上。
|
||||
"""
|
||||
comp.CreateItem(playerId).SpawnItemToPlayerInv(
|
||||
{"itemName": itemName, "count": count, "auxValue": 0}, playerId)
|
||||
+39
-257
@@ -12,6 +12,15 @@
|
||||
玩家右键烹饪方块 → 引擎查 RecipeData 有没有匹配的配方 →
|
||||
有就消耗手上物品 → 替换方块/记录翻炒进度 → 完成做菜
|
||||
|
||||
【为什么和工具切换分开】
|
||||
原本做菜和普通工具方块切换(箭桶、水槽、书架、药水架等)都在这个类里。
|
||||
现在已拆成两个独立系统:
|
||||
- CookingService → 只处理做菜配方(RECIPES),本文件
|
||||
- BlockSwitchService → 只处理工具方块切换(BLOCK_SWITCHES)
|
||||
两个系统都监听同一个 ServerBlockUseEvent,
|
||||
各自在事件第一行用 cookBlockSet/switchBlockSet(O(1) 的 set)按方块名拦截,
|
||||
不是自己的方块直接 return,互不干扰,各自只查自己那一份数据。
|
||||
|
||||
【设计原则】
|
||||
- 加新菜不改代码:在 RecipeData.py 加一条配方就行,这里一行不用动
|
||||
- 所有做菜逻辑一处维护:炒锅、煎锅、油锅、架锅、蒸笼……全走同一个引擎
|
||||
@@ -49,11 +58,6 @@ blockEntityDataComp = comp.CreateBlockEntityData(LevelId)
|
||||
# RECIPES 是一个列表,每条配方是一个 dict,格式见 RecipeData.py 顶部注释
|
||||
from ..RecipeData import RECIPES, inferBaseBlock, BELOW_CAMPFIRE
|
||||
|
||||
# ---- 导入工具方块切换规则 ----
|
||||
# BLOCK_SWITCHES 是一个列表,每条规则是一个 dict,格式见 BlockSwitchData.py 顶部注释
|
||||
from ..BlockSwitchData import BLOCK_SWITCHES
|
||||
|
||||
|
||||
# ================================================================================
|
||||
# 工具函数:读取物品 userData 里的嵌套字段
|
||||
# ================================================================================
|
||||
@@ -111,14 +115,8 @@ class CookingService(BlueFoxServerSystem):
|
||||
2. 方块实体数据(blockEntityData)—— 慢一点,但关服重启后还在
|
||||
每次操作都会同步更新两处,保证重启不丢进度
|
||||
|
||||
【工具方块记录数据】
|
||||
switchRecord = {
|
||||
(维度ID, x, y, z): {
|
||||
"count": 整数, # recordType="count" 时记录数量(如箭桶里有多少箭)
|
||||
"itemName": 字符串, # recordType="count" 时记录物品名(如"minecraft:arrow")
|
||||
"stack": [物品信息列表], # recordType="stack" 时记录后进先出的物品堆
|
||||
}
|
||||
}
|
||||
工具方块(箭桶、书架、药水架等)的记录数据已拆分到 BlockSwitchService,
|
||||
本类只负责做菜配方(RECIPES)。
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, systemName):
|
||||
@@ -128,22 +126,24 @@ class CookingService(BlueFoxServerSystem):
|
||||
super(CookingService, self).__init__(namespace, systemName)
|
||||
print "===== CookingService init ====="
|
||||
self.stirData = {} # 内存中的翻炒数据字典
|
||||
self.switchRecord = {} # 工具方块的记录数据(箭桶数量、药水架顺序等)
|
||||
self.switchCooldown = {} # 工具方块切换冷却时间(防止连点)
|
||||
self._cancelItemUse = {} # 将要取消下一次物品使用的玩家ID集合(防止喝药水等)
|
||||
self.ListenEvent() # 注册游戏事件监听
|
||||
|
||||
# ---- 构建统一配方字典(O(1) 查找) ----
|
||||
# 把 RECIPES(做菜配方)和 BLOCK_SWITCHES(工具切换规则)合并成一份字典
|
||||
# ---- 构建做菜配方字典(O(1) 查找) ----
|
||||
# 只含 RECIPES(做菜配方),工具方块切换已拆到 BlockSwitchService
|
||||
# key = (方块名, 物品名),value = 配方dict
|
||||
# 这样查找时不用遍历整个列表,直接 dict[key] 就能拿到,速度飞快
|
||||
self.allRecipes = {}
|
||||
for recipe in RECIPES:
|
||||
key = (recipe["source"], recipe.get("item"))
|
||||
self.allRecipes[key] = recipe
|
||||
for switch in BLOCK_SWITCHES:
|
||||
key = (switch["source"], switch.get("item"))
|
||||
self.allRecipes[key] = switch
|
||||
|
||||
# ---- 构建做菜方块名集合(O(1) 快速拦截,第一道过滤) ----
|
||||
# 事件进来先判断方块名在不在这个集合里,不在直接 return,
|
||||
# 这样工具方块的右键事件根本不会进到本系统的字典查找
|
||||
self.cookBlockSet = set()
|
||||
for recipe in RECIPES:
|
||||
self.cookBlockSet.add(recipe["source"])
|
||||
|
||||
def Destroy(self):
|
||||
"""
|
||||
@@ -200,10 +200,11 @@ class CookingService(BlueFoxServerSystem):
|
||||
|
||||
流程:
|
||||
1. 拿到被右键的方块名、玩家ID、方块坐标
|
||||
2. 看玩家手里拿了什么物品
|
||||
3. 拿着"方块名+手里物品名"去统一字典里 O(1) 查找
|
||||
4. 找到规则后,判断是"做菜配方"还是"工具切换"类型
|
||||
5. 执行对应的逻辑
|
||||
2. 先按方块名判断是不是本系统管辖的做菜方块(O(1) set 拦截)
|
||||
- 不是 → 直接 return(这是工具方块的右键,交给 BlockSwitchService 处理)
|
||||
3. 看玩家手里拿了什么物品
|
||||
4. 拿着"方块名+手里物品名"去做菜字典里 O(1) 查找
|
||||
5. 命中配方 → 查底座 → 执行做菜逻辑(翻炒/替换)
|
||||
|
||||
args 是网易API传过来的事件参数,包含:
|
||||
- blockName: 被右键的方块名字(如 "rtdd:wok")
|
||||
@@ -218,15 +219,20 @@ class CookingService(BlueFoxServerSystem):
|
||||
dim = comp.CreateDimension(playerId).GetEntityDimensionId() # 玩家在哪个维度(主世界/下界/末地)
|
||||
key = (dim, x, y, z) # 完整的唯一标识:维度+坐标
|
||||
|
||||
# ---- 第二步:拿到玩家手里拿的物品 ----
|
||||
# ---- 第二步:O(1) 方块名拦截 ----
|
||||
# 只有做菜方块的右键才归本系统处理;工具方块直接放过,交给 BlockSwitchService
|
||||
if blockName not in self.cookBlockSet:
|
||||
return
|
||||
|
||||
# ---- 第三步:拿到玩家手里拿的物品 ----
|
||||
itemDict = self.getCarriedItem(playerId)
|
||||
# itemDict 可能是 None(空手),也可能是 dict(手里有东西)
|
||||
# dict 格式:{"newItemName": "rtdd:spatula", "count": 1, "userData": {...}}
|
||||
|
||||
# ---- 第三步:查配方(O(1) 字典查找) ----
|
||||
# ---- 第四步:查做菜配方(O(1) 字典查找) ----
|
||||
recipe = self.findRecipe(blockName, itemDict)
|
||||
if not recipe:
|
||||
# 没找到匹配的配方 → 不是做菜操作,不拦截,让游戏正常处理
|
||||
# 方块是烹饪方块,但当前组合没有匹配的配方(比如手里物品不对)→ 不拦截
|
||||
return
|
||||
|
||||
# 找到配方后,取消默认行为(如喝药水、吃食物等),由本 Mod 全权接管
|
||||
@@ -234,26 +240,15 @@ class CookingService(BlueFoxServerSystem):
|
||||
# 同时标记该玩家下一次物品使用事件也要取消,防止喝药水等默认行为
|
||||
self._cancelItemUse[playerId] = True
|
||||
|
||||
# ---- 第四步:判断是不是工具切换类型 ----
|
||||
recipeType = recipe.get("type", "cook")
|
||||
# type="switch" 或 "timer" → 工具方块切换(箭桶、挂肉钩、水槽等)
|
||||
# 没有 type 或 type="cook" → 做菜配方(炒锅、煎锅等)
|
||||
|
||||
# ---- 统一加载:把方块里的历史数据恢复到内存 ----
|
||||
# 不管是做菜还是工具切换,先把方块数据加载到内存,后面直接用
|
||||
# ---- 第五步:加载翻炒历史数据到内存 ----
|
||||
# 这样 handleStir 里直接从 self.stirData 取就行
|
||||
self._loadBlockDataToMemory(key, playerId, pos, dim, recipe)
|
||||
|
||||
if recipeType == "switch" or recipeType == "timer":
|
||||
# 工具方块切换(水槽、箭桶、挂肉钩、盘子架等)不做底座检查,直接执行
|
||||
self.handleSwitch(recipe, playerId, pos, dim, itemDict, key, blockName)
|
||||
return
|
||||
|
||||
# ---- 底座检查:仅做菜配方才需要,锅下面必须有灶台/营火 ----
|
||||
# 工具切换类型在上面已提前返回,不会走到这里
|
||||
# ---- 底座检查:锅下面必须有灶台/营火 ----
|
||||
if not self.checkBaseBlock(recipe, playerId, pos, dim):
|
||||
return
|
||||
|
||||
# ---- 第五步:以下为做菜配方逻辑 ----
|
||||
# ---- 第六步:以下为做菜配方逻辑 ----
|
||||
|
||||
# 内存中已有数据(_loadBlockDataToMemory 已加载),直接用
|
||||
stirInfo = self.stirData.get(key, {})
|
||||
@@ -315,152 +310,6 @@ class CookingService(BlueFoxServerSystem):
|
||||
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# 工具方块切换处理
|
||||
# ============================================================================
|
||||
|
||||
def handleSwitch(self, recipe, playerId, pos, dim, itemDict, key, blockName):
|
||||
"""
|
||||
处理工具方块的切换逻辑(箭桶、挂肉钩、水槽、盘子架、药水架、书柜等)。
|
||||
|
||||
支持三种模式:
|
||||
1. recordType="count" → 记录一种物品+数量(如箭桶存箭)
|
||||
2. recordType="stack" → 后进先出堆栈(如药水架、书柜)
|
||||
3. 无 record → 纯切换,不记录(如挂肉钩直接换状态)
|
||||
|
||||
流程:
|
||||
1. 如果是计时型(type="timer"),直接替换方块,不消耗物品
|
||||
2. 如果是切换型(type="switch"),根据 recordType 决定记录方式
|
||||
3. 消耗物品、替换方块、更新记录
|
||||
"""
|
||||
# ---- 如果规则指定了 tool,检查玩家手里是不是拿着指定工具 ----
|
||||
if recipe.get("tool"):
|
||||
if not itemDict:
|
||||
return
|
||||
carriedItem = itemDict.get("newItemName", "")
|
||||
# 简单判断:物品名是否包含工具名
|
||||
if recipe["tool"] not in carriedItem:
|
||||
return
|
||||
|
||||
# ---- 冷却检查:防止连点右键瞬间切换多次 ----
|
||||
now = time.time()
|
||||
lastOp = self.switchCooldown.get(key, 0)
|
||||
cd = recipe.get("switchCd", 0.5)
|
||||
if now - lastOp < cd:
|
||||
return
|
||||
self.switchCooldown[key] = now
|
||||
|
||||
# ---- 计时型:直接替换方块,不消耗物品,不记录 ----
|
||||
if recipe.get("type") == "timer":
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
return
|
||||
|
||||
# ---- 切换型 ----
|
||||
# 内存中已有数据(_loadBlockDataToMemory 已加载),直接用
|
||||
record = self.switchRecord.get(key)
|
||||
|
||||
# ---- 正向操作:往方块里放东西(source == 被右键的方块) ----
|
||||
if recipe["source"] == blockName and recipe.get("item") is not None:
|
||||
# 玩家手里有物品,往方块里放
|
||||
needSave = False # 标记是否需要保存记录到方块
|
||||
if recipe.get("record"):
|
||||
if recipe.get("recordType") == "count":
|
||||
# 数量记录型:只记录一种物品的总数
|
||||
if not record:
|
||||
record = {
|
||||
"count": 0,
|
||||
"itemName": itemDict["newItemName"],
|
||||
}
|
||||
# 检查是否满了
|
||||
maxCount = recipe.get("maxCount", 999)
|
||||
if record["count"] >= maxCount:
|
||||
return
|
||||
# 记录物品+1
|
||||
record["count"] += 1
|
||||
needSave = True
|
||||
|
||||
elif recipe.get("recordType") == "stack":
|
||||
# 堆栈记录型:后进先出,记录每种物品的完整信息
|
||||
if not record:
|
||||
record = {"stack": []}
|
||||
maxCount = recipe.get("maxCount", 999)
|
||||
if len(record["stack"]) >= maxCount:
|
||||
return
|
||||
# 把物品信息压入堆栈
|
||||
record["stack"].append({
|
||||
"itemName": itemDict["newItemName"],
|
||||
"count": itemDict.get("count", 1),
|
||||
"auxValue": itemDict.get("newAuxValue", 0),
|
||||
"userData": itemDict.get("userData", {}),
|
||||
})
|
||||
needSave = True
|
||||
|
||||
# 消耗物品
|
||||
if recipe.get("consume", 1) > 0:
|
||||
self.consumeItem(playerId, itemDict, recipe.get("consume", 1))
|
||||
|
||||
# 替换方块(旧方块销毁,新方块出现)
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# ★ 关键:替换完方块后再保存记录到新方块上,否则数据随旧方块一起销毁
|
||||
if needSave:
|
||||
self._saveRecord(key, playerId, pos, dim, record)
|
||||
|
||||
# 如果配方指定了给物品(如烧烤架浇水给桶)
|
||||
if recipe.get("giveItem"):
|
||||
self.giveItem(playerId, recipe["giveItem"])
|
||||
|
||||
return
|
||||
|
||||
# ---- 逆向操作:从方块里取东西(空手右键或取回物品) ----
|
||||
if recipe["source"] == blockName and recipe.get("item") is None:
|
||||
needSaveRecord = False # 是否需要保存记录
|
||||
needClearRecord = False # 是否需要清除记录
|
||||
|
||||
if recipe.get("record") and record:
|
||||
if recipe.get("recordType") == "count":
|
||||
# 数量记录型取回:给玩家对应的物品,数量-1
|
||||
if record["count"] > 0:
|
||||
self.giveItem(playerId, record["itemName"])
|
||||
record["count"] -= 1
|
||||
if record["count"] <= 0:
|
||||
needClearRecord = True
|
||||
else:
|
||||
needSaveRecord = True
|
||||
|
||||
elif recipe.get("recordType") == "stack":
|
||||
# 堆栈记录型取回:后进先出,取最后一个放的物品
|
||||
if record["stack"]:
|
||||
topItem = record["stack"].pop()
|
||||
comp.CreateItem(playerId).SpawnItemToPlayerInv(
|
||||
{"itemName": topItem["itemName"],
|
||||
"count": topItem["count"],
|
||||
"auxValue": topItem["auxValue"],
|
||||
"userData": topItem.get("userData", {})},
|
||||
playerId)
|
||||
if not record["stack"]:
|
||||
needClearRecord = True
|
||||
else:
|
||||
needSaveRecord = True
|
||||
|
||||
# 如果配方直接指定了给什么物品(水槽、盘子架取回、挂肉钩退回等)
|
||||
if recipe.get("giveItem"):
|
||||
self.giveItem(playerId, recipe["giveItem"])
|
||||
|
||||
# 替换方块(旧方块销毁,新方块出现)
|
||||
if recipe.get("target"):
|
||||
self.replaceBlock(pos, dim, recipe["target"], playerId)
|
||||
|
||||
# ★ 关键:替换完方块后再保存/清除记录,否则数据随旧方块一起销毁
|
||||
if needClearRecord:
|
||||
self.clearSwitchRecord(key, playerId, pos, dim)
|
||||
elif needSaveRecord:
|
||||
self._saveRecord(key, playerId, pos, dim, record)
|
||||
|
||||
return
|
||||
|
||||
# ============================================================================
|
||||
# 配方执行(普通替换类型)
|
||||
# ============================================================================
|
||||
@@ -810,38 +659,20 @@ class CookingService(BlueFoxServerSystem):
|
||||
except Exception as e:
|
||||
print "[CookingService] _writeBlockData 写入失败: pos=%s dim=%s err=%s" % (pos, dim, str(e))
|
||||
|
||||
def _saveRecord(self, key, playerId, pos, dim, record):
|
||||
"""
|
||||
同时更新内存和方块实体数据中的工具方块记录。
|
||||
|
||||
替代原本重复出现的:
|
||||
self.switchRecord[key] = record
|
||||
self.saveSwitchRecord(playerId, pos, dim, record)
|
||||
"""
|
||||
self.switchRecord[key] = record
|
||||
self.saveSwitchRecord(playerId, pos, dim, record)
|
||||
|
||||
# ============================================================================
|
||||
# 统一数据加载入口(玩家右键方块时先把方块数据恢复到内存)
|
||||
# ============================================================================
|
||||
|
||||
def _loadBlockDataToMemory(self, key, playerId, pos, dim, recipe):
|
||||
"""
|
||||
在确定玩家右键命中了一个配方后,立刻把方块里存的历史数据加载到内存。
|
||||
在确定玩家右键命中了一个做菜配方后,立刻把方块里存的翻炒进度加载到内存。
|
||||
|
||||
这样 handleStir / handleSwitch 里就不用各自加载了,
|
||||
直接从 self.stirData / self.switchRecord 取就行。
|
||||
|
||||
两个数据源互不干扰:翻炒数据用 stirData,工具切换记录用 switchRecord。
|
||||
这样 handleStir 里就不用自己加载了,直接从 self.stirData 取就行。
|
||||
"""
|
||||
if key not in self.stirData:
|
||||
stirInfo = self.loadStirData(playerId, pos, dim, recipe)
|
||||
if stirInfo:
|
||||
self.stirData[key] = stirInfo
|
||||
if key not in self.switchRecord:
|
||||
switchInfo = self.loadSwitchRecord(playerId, pos, dim)
|
||||
if switchInfo:
|
||||
self.switchRecord[key] = switchInfo
|
||||
|
||||
# ============================================================================
|
||||
# 翻炒数据持久化(存方块里,不存JSON文件)
|
||||
@@ -894,55 +725,6 @@ class CookingService(BlueFoxServerSystem):
|
||||
self.stirData.pop(key, None)
|
||||
self._writeBlockData(dim, pos, rtdd_sc=0, rtdd_slt=0.0, rtdd_sf=False)
|
||||
|
||||
# ============================================================================
|
||||
# 工具方块记录持久化(药水架、书柜、箭桶等)
|
||||
# ============================================================================
|
||||
|
||||
def saveSwitchRecord(self, playerId, pos, dim, record):
|
||||
"""
|
||||
把工具方块的记录数据写入方块实体数据。
|
||||
记录类型:count型(箭桶)→ rtdd_swt/swc/swi,stack型(书柜/药水架)→ rtdd_swt/sws
|
||||
"""
|
||||
if "count" in record:
|
||||
self._writeBlockData(dim, pos,
|
||||
rtdd_swt="count",
|
||||
rtdd_swc=record["count"],
|
||||
rtdd_swi=record.get("itemName", ""))
|
||||
elif "stack" in record:
|
||||
self._writeBlockData(dim, pos,
|
||||
rtdd_swt="stack",
|
||||
rtdd_sws=record["stack"])
|
||||
|
||||
def loadSwitchRecord(self, playerId, pos, dim):
|
||||
"""
|
||||
从方块实体数据里读取工具方块的记录数据。
|
||||
|
||||
返回:
|
||||
有数据 → {"count": N, "itemName": "xxx"} 或 {"stack": [...]}
|
||||
没有 → None
|
||||
"""
|
||||
try:
|
||||
blockData = blockEntityDataComp.GetBlockEntityData(dim, pos)
|
||||
swt = blockData["rtdd_swt"] # 记录类型:"count" 或 "stack"
|
||||
if swt == "count":
|
||||
return {
|
||||
"count": blockData["rtdd_swc"],
|
||||
"itemName": blockData["rtdd_swi"],
|
||||
}
|
||||
elif swt == "stack":
|
||||
return {
|
||||
"stack": blockData["rtdd_sws"],
|
||||
}
|
||||
except:
|
||||
return None
|
||||
|
||||
def clearSwitchRecord(self, key, playerId, pos, dim):
|
||||
"""
|
||||
清除工具方块的记录数据(内存+方块实体数据)。
|
||||
"""
|
||||
self.switchRecord.pop(key, None)
|
||||
self._writeBlockData(dim, pos, rtdd_swt="", rtdd_swc=0, rtdd_swi="", rtdd_sws=[])
|
||||
|
||||
# ============================================================================
|
||||
# 拆方块处理
|
||||
# ============================================================================
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from BlueFoxTools.ModLoader import *
|
||||
from ceshi.service.CookingService import CookingService
|
||||
from ceshi.service.BlockSwitchService import BlockSwitchService
|
||||
|
||||
if not serverApi.IsInServer():
|
||||
pass
|
||||
|
||||
@@ -6,28 +6,28 @@
|
||||
"value": 0
|
||||
},
|
||||
"netease:aabb": {
|
||||
"clip": {
|
||||
"max": [
|
||||
0.2,
|
||||
1.45,
|
||||
0.9
|
||||
],
|
||||
"collision": {
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0.3
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
0.95,
|
||||
0.75
|
||||
]
|
||||
},
|
||||
"collision": {
|
||||
"max": [
|
||||
0.2,
|
||||
1.45,
|
||||
0.9
|
||||
],
|
||||
"clip": {
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0.3
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
0.95,
|
||||
0.75
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user