359 lines
15 KiB
Python
359 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
||
#
|
||
# 一盒钉子(dn_sa:box_of_nails):物品 + 方块双形态的补给容器
|
||
# - 物品耐久用于显示盒内钉子数:max_damage = 65,remaining = 钉子数 + 1
|
||
# (空盒 remaining = 1,满盒 remaining = 65;+1 是为了避免耐久归零导致物品损坏)
|
||
# - 手持盒子右键方块:放置为 dn_sa:box_of_nails_block(面向跟随玩家朝向),盒内钉子数存入世界存档(按坐标索引)
|
||
# - 右键方块:手持钉子时放入(潜行放满,否则放 1 颗);否则取出(潜行全部取出,否则取 1 颗)
|
||
# - 破坏方块:还原为一个物品,耐久保留盒内钉子数
|
||
# - 描述("已存储:x/64")走物品的 customTips:生成物品时写进 itemDict,之后由
|
||
# ChangePlayerItemTipsAndExtraId 覆盖刷新。customTips 会直接覆盖物品 JSON 里的静态文案,
|
||
# 所以 JSON 里那行 "已存储:1/64" 只是还没刷新时的兜底显示。
|
||
# 注意:读玩家背包必须用 GetPlayerItem —— GetEntityItem 只适用于生物/左右手/装备,
|
||
# 官方文档写明"背包不行",用它读背包会一直返回 None,导致描述永远停在 JSON 的静态文案。
|
||
import math
|
||
import time
|
||
|
||
import mod.server.extraServerApi as serverApi
|
||
|
||
compFactory = serverApi.GetEngineCompFactory()
|
||
|
||
# 测试参数:与 modMain 共用开关与探针(print + 写文件),仅在 DEBUG_TEST 为 True 时输出
|
||
try:
|
||
from Script_dn_sa.modMain import DEBUG_TEST, probe
|
||
except Exception:
|
||
DEBUG_TEST = False
|
||
|
||
|
||
def probe(msg):
|
||
pass
|
||
|
||
ITEM_ID = "dn_sa:box_of_nails"
|
||
BLOCK_ID = "dn_sa:box_of_nails_block"
|
||
NAILS_ID = "dn_sa:nails"
|
||
BLOCK_AIR = "minecraft:air"
|
||
|
||
NAILS_MAX_STACK = 64 # 单叠钉子上限
|
||
MAX_CHARGES = 64 # 钉子盒容量
|
||
MAX_DAMAGE = MAX_CHARGES + 1 # 物品最大耐久
|
||
CONTENTS_KEY = "dn_sa_box_of_nails_contents" # {"维度:x,y,z": 钉子数}
|
||
|
||
# ServerBlockUseEvent 在按住右键时逐 tick 触发,用冷却间隔避免一次长按连续存取
|
||
COOLDOWN_SECONDS = 0.25 # 每次成功存取后的冷却时间
|
||
|
||
# 物品描述(末行动态显示当前钉子数)
|
||
TIPS_TEXT = "手持钉子点击放入,空手点击取出\n包含:钉子\n已存储:%d/%d"
|
||
|
||
# 点击面 -> 法线偏移(0下 1上 2北 3南 4西 5东)
|
||
FACE_OFFSET = {
|
||
0: (0, -1, 0),
|
||
1: (0, 1, 0),
|
||
2: (0, 0, -1),
|
||
3: (0, 0, 1),
|
||
4: (-1, 0, 0),
|
||
5: (1, 0, 0),
|
||
}
|
||
|
||
ITEM_POS = serverApi.GetMinecraftEnum().ItemPosType
|
||
INVENTORY = ITEM_POS.INVENTORY
|
||
CARRIED = ITEM_POS.CARRIED
|
||
|
||
|
||
def clamp(v, lo, hi):
|
||
return lo if v < lo else (hi if v > hi else v)
|
||
|
||
|
||
class BoxOfNailsServer(serverApi.GetServerSystemCls()):
|
||
def __init__(self, namespace, name):
|
||
super(BoxOfNailsServer, self).__init__(namespace, name)
|
||
if DEBUG_TEST:
|
||
probe("3/4 BoxOfNailsServer 系统实例已创建")
|
||
self.levelId = serverApi.GetLevelId()
|
||
self._lastUse = {} # playerId -> 上次成功存取的时间戳(秒)
|
||
self._tipsSet = {} # (playerId, slot) -> 已写入的描述文本,防重复写入
|
||
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
|
||
"AddServerPlayerEvent", self, self.OnPlayerJoin)
|
||
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
|
||
"ServerItemUseOnEvent", self, self.OnItemUseOn)
|
||
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
|
||
"ServerBlockUseEvent", self, self.OnBlockUse)
|
||
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
|
||
"ServerPlayerTryDestroyBlockEvent", self, self.OnTryDestroy)
|
||
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(),
|
||
"InventoryItemChangedServerEvent", self, self.OnInventoryChanged)
|
||
|
||
# ---------- 基础工具 ----------
|
||
def _blockComp(self):
|
||
return compFactory.CreateBlockInfo(self.levelId)
|
||
|
||
def _extraComp(self):
|
||
return compFactory.CreateExtraData(self.levelId)
|
||
|
||
@staticmethod
|
||
def _key(dim, x, y, z):
|
||
return "%d:%d,%d,%d" % (dim, x, y, z)
|
||
|
||
@staticmethod
|
||
def _itemName(itemDict):
|
||
"""兼容读取物品 identifier:新版物品字典为 newItemName,旧版为 itemName。"""
|
||
if not itemDict:
|
||
return None
|
||
return itemDict.get("newItemName") or itemDict.get("itemName")
|
||
|
||
def _loadContents(self):
|
||
data = self._extraComp().GetExtraData(CONTENTS_KEY)
|
||
return data if isinstance(data, dict) else {}
|
||
|
||
def _saveContents(self, data):
|
||
self._extraComp().SetExtraData(CONTENTS_KEY, data, True)
|
||
|
||
def _getBlockName(self, x, y, z, dim):
|
||
info = self._blockComp().GetBlockNew((x, y, z), dim)
|
||
if not info:
|
||
return None
|
||
return info.get("name")
|
||
|
||
def _setBlock(self, x, y, z, dim, name, aux=0):
|
||
# oldBlockHandling=0(替换),isLegacy=True,updateNeighbors=False
|
||
self._blockComp().SetBlockNew((x, y, z), {"name": name, "aux": aux}, 0, dim, True, False)
|
||
|
||
@staticmethod
|
||
def _facingAux(playerId):
|
||
"""按玩家面朝方向计算四面向 aux(南=0/西=1/北=2/东=3)。
|
||
|
||
组件 netease:face_directional 的面向由 SetBlockNew 的 auxValue 决定。
|
||
玩家 yaw:0=南(+Z)、90=西(-X)、180=北(-Z)、270=东(+X);
|
||
放置时方块「北面」朝向玩家,即方块面向 = 玩家准心方向的反方向。
|
||
"""
|
||
try:
|
||
yaw = float(compFactory.CreateRot(playerId).GetRot()[1]) % 360.0
|
||
except Exception:
|
||
return 2
|
||
quad = int(math.floor(yaw / 90.0 + 0.5)) % 4
|
||
return (quad + 2) % 4
|
||
|
||
# ---------- 物品耐久 <-> 钉子数 ----------
|
||
@staticmethod
|
||
def _chargesOfRemaining(remaining):
|
||
return clamp(int(remaining) - 1, 0, MAX_CHARGES)
|
||
|
||
@staticmethod
|
||
def _remainingOfCharges(charges):
|
||
return clamp(int(charges), 0, MAX_CHARGES) + 1
|
||
|
||
def _handCharges(self, playerId):
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
return self._chargesOfRemaining(itemComp.GetItemDurability(CARRIED, 0))
|
||
|
||
def _consumeOne(self, playerId):
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
slotId = itemComp.GetSelectSlotId()
|
||
itemDict = itemComp.GetPlayerItem(INVENTORY, slotId, True)
|
||
if not itemDict:
|
||
return
|
||
count = itemDict.get("count", 1)
|
||
itemComp.SetInvItemNum(slotId, count - 1 if count > 1 else 0)
|
||
|
||
def _giveBox(self, playerId, charges):
|
||
"""给予带指定钉子数的盒子;优先放入第一个空槽,便于写入耐久。"""
|
||
if not playerId:
|
||
return
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
remainingOfCharges = self._remainingOfCharges(charges)
|
||
itemDict = {
|
||
"itemName": ITEM_ID, "count": 1, "auxValue": 0, 'customTips': TIPS_TEXT % (remainingOfCharges - 1, 64),
|
||
'durability': remainingOfCharges
|
||
}
|
||
slot = -1
|
||
items = itemComp.GetPlayerAllItems(INVENTORY, False) or []
|
||
for i, it in enumerate(items):
|
||
if it is None:
|
||
slot = i
|
||
break
|
||
if slot < 0:
|
||
itemComp.SpawnItemToPlayerInv(itemDict, playerId, -1)
|
||
return
|
||
itemComp.SpawnItemToPlayerInv(itemDict, playerId, slot)
|
||
itemComp.SetItemDurability(INVENTORY, slot, remainingOfCharges)
|
||
self._refreshBoxTips(playerId, slot)
|
||
|
||
# ---------- 动态描述(显示盒内钉子数) ----------
|
||
@staticmethod
|
||
def _boxTips(charges):
|
||
return TIPS_TEXT % (clamp(int(charges), 0, MAX_CHARGES), MAX_CHARGES)
|
||
|
||
def _refreshBoxTips(self, playerId, slot):
|
||
"""把指定槽位盒子物品的描述更新为当前钉子数。"""
|
||
if not playerId:
|
||
return
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
key = (playerId, slot)
|
||
itemDict = itemComp.GetEntityItem(INVENTORY, slot, True)
|
||
if not itemDict or self._itemName(itemDict) != ITEM_ID:
|
||
self._tipsSet.pop(key, None)
|
||
return
|
||
tips = self._boxTips(self._chargesOfRemaining(itemComp.GetItemDurability(INVENTORY, slot)))
|
||
if self._tipsSet.get(key) == tips:
|
||
return
|
||
itemComp.ChangePlayerItemTipsAndExtraId(INVENTORY, slot, tips, "box1")
|
||
self._tipsSet[key] = tips
|
||
|
||
def OnInventoryChanged(self, args):
|
||
newItem = args.get("newItemDict")
|
||
if not newItem or self._itemName(newItem) != ITEM_ID:
|
||
return
|
||
self._refreshBoxTips(args.get("playerId"), args.get("slot", -1))
|
||
|
||
def _notifyOne(self, playerId, text):
|
||
"""测试参数用:给玩家发一条聊天提示(仅在 DEBUG_TEST 开启时调用)。"""
|
||
if not playerId:
|
||
return
|
||
try:
|
||
compFactory.CreateMsg(playerId).NotifyOneMessage(playerId, text)
|
||
except Exception:
|
||
pass
|
||
|
||
def OnPlayerJoin(self, args):
|
||
"""玩家进入世界时刷新背包里所有盒子的描述。"""
|
||
playerId = args.get("id")
|
||
if DEBUG_TEST:
|
||
probe("4/4 AddServerPlayerEvent 已触发:脚本与事件监听均正常")
|
||
self._notifyOne(playerId, "[dn_sa] 脚本已加载,事件监听正常")
|
||
self._refreshAllBoxTips(playerId)
|
||
|
||
def _refreshAllBoxTips(self, playerId):
|
||
if not playerId:
|
||
return
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
items = itemComp.GetPlayerAllItems(INVENTORY, False) or []
|
||
for slot, it in enumerate(items):
|
||
if it and self._itemName(it) == ITEM_ID:
|
||
self._refreshBoxTips(playerId, slot)
|
||
|
||
# ---------- 放入 / 取出 ----------
|
||
def _depositNails(self, playerId, dim, x, y, z, handCount, sneaking):
|
||
"""把手中钉子放入盒子(潜行=放满,否则=1 颗);返回是否放入成功。"""
|
||
key = self._key(dim, x, y, z)
|
||
contents = self._loadContents()
|
||
charges = clamp(contents.get(key, 0), 0, MAX_CHARGES)
|
||
move = clamp(handCount if sneaking else 1, 0, MAX_CHARGES - charges)
|
||
if move <= 0:
|
||
return False
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
itemComp.SetInvItemNum(itemComp.GetSelectSlotId(), handCount - move)
|
||
contents[key] = charges + move
|
||
self._saveContents(contents)
|
||
self._lastUse[playerId] = time.time()
|
||
return True
|
||
|
||
def _withdrawNails(self, playerId, dim, x, y, z, sneaking):
|
||
"""从盒子取出钉子(潜行=全部取出,否则=1 颗);返回是否取出成功。"""
|
||
key = self._key(dim, x, y, z)
|
||
contents = self._loadContents()
|
||
charges = clamp(contents.get(key, 0), 0, MAX_CHARGES)
|
||
if charges <= 0:
|
||
return False
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
capacity = 0
|
||
for it in (itemComp.GetPlayerAllItems(INVENTORY, False) or []):
|
||
if it is None:
|
||
capacity += NAILS_MAX_STACK
|
||
elif self._itemName(it) == NAILS_ID:
|
||
capacity += NAILS_MAX_STACK - it.get("count", 0)
|
||
take = clamp(charges if sneaking else 1, 0, capacity)
|
||
if take <= 0:
|
||
return False
|
||
itemComp.SpawnItemToPlayerInv({"itemName": NAILS_ID, "count": take, "auxValue": 0}, playerId, -1)
|
||
contents[key] = charges - take
|
||
self._saveContents(contents)
|
||
self._lastUse[playerId] = time.time()
|
||
return True
|
||
|
||
def _isSneaking(self, playerId):
|
||
try:
|
||
return bool(compFactory.CreatePlayer(playerId).isSneaking())
|
||
except Exception:
|
||
return False
|
||
|
||
# ---------- 放置 ----------
|
||
def OnItemUseOn(self, args):
|
||
itemDict = args.get("itemDict") or {}
|
||
itemName = self._itemName(itemDict)
|
||
playerId = args.get("entityId")
|
||
if not playerId:
|
||
return
|
||
# 手持钉子点击盒子方块 -> 放入(部分情况下潜行只触发本事件,作为兼容入口)
|
||
if itemName == NAILS_ID and args.get("blockName") == BLOCK_ID:
|
||
if time.time() - self._lastUse.get(playerId, 0.0) < COOLDOWN_SECONDS:
|
||
return
|
||
ok = self._depositNails(playerId, args.get("dimensionId", 0),
|
||
args.get("x", 0), args.get("y", 0), args.get("z", 0),
|
||
itemDict.get("count", 0), self._isSneaking(playerId))
|
||
if ok:
|
||
args["ret"] = True
|
||
return
|
||
if itemName != ITEM_ID:
|
||
return
|
||
off = FACE_OFFSET.get(args.get("face", 0))
|
||
if off is None:
|
||
return
|
||
# 点击的正是钉子盒方块时不放置,避免在盒子上再叠一个盒子
|
||
if args.get("blockName") == BLOCK_ID:
|
||
return
|
||
dim = args.get("dimensionId", 0)
|
||
tx = args.get("x", 0) + off[0]
|
||
ty = args.get("y", 0) + off[1]
|
||
tz = args.get("z", 0) + off[2]
|
||
if self._getBlockName(tx, ty, tz, dim) != BLOCK_AIR:
|
||
return
|
||
charges = self._handCharges(playerId)
|
||
self._setBlock(tx, ty, tz, dim, BLOCK_ID, self._facingAux(playerId))
|
||
contents = self._loadContents()
|
||
contents[self._key(dim, tx, ty, tz)] = charges
|
||
self._saveContents(contents)
|
||
self._consumeOne(playerId)
|
||
# 记录放置时刻:避免按住右键时同一动作紧接着触发 ServerBlockUseEvent 误取出钉子
|
||
self._lastUse[playerId] = time.time()
|
||
args["ret"] = True
|
||
|
||
# ---------- 存取 ----------
|
||
def OnBlockUse(self, args):
|
||
if args.get("blockName") != BLOCK_ID:
|
||
return
|
||
playerId = args.get("playerId")
|
||
if not playerId:
|
||
return
|
||
args["cancel"] = True
|
||
# 该事件按住右键时逐 tick 触发,冷却期内不再处理,避免一次长按连续存取
|
||
if time.time() - self._lastUse.get(playerId, 0.0) < COOLDOWN_SECONDS:
|
||
return
|
||
dim = args.get("dimensionId", 0)
|
||
x, y, z = args.get("x", 0), args.get("y", 0), args.get("z", 0)
|
||
# 手上/事件中使用的物品:优先用事件自带的物品信息,更准确
|
||
itemDict = args.get("itemDict") or {}
|
||
itemComp = compFactory.CreateItem(playerId)
|
||
hand = itemComp.GetPlayerItem(INVENTORY, itemComp.GetSelectSlotId(), True) or {}
|
||
usedName = self._itemName(itemDict) or self._itemName(hand)
|
||
# 手持盒子物品时不做存取(属于放置动作的延续,避免误取出钉子)
|
||
if usedName == ITEM_ID:
|
||
return
|
||
sneaking = self._isSneaking(playerId)
|
||
if usedName == NAILS_ID:
|
||
self._depositNails(playerId, dim, x, y, z,
|
||
itemDict.get("count") or hand.get("count", 0), sneaking)
|
||
else:
|
||
self._withdrawNails(playerId, dim, x, y, z, sneaking)
|
||
|
||
# ---------- 破坏 ----------
|
||
def OnTryDestroy(self, args):
|
||
if args.get("fullName") != BLOCK_ID:
|
||
return
|
||
dim = args.get("dimensionId", 0)
|
||
key = self._key(dim, args.get("x", 0), args.get("y", 0), args.get("z", 0))
|
||
contents = self._loadContents()
|
||
charges = clamp(contents.pop(key, 0), 0, MAX_CHARGES)
|
||
self._saveContents(contents)
|
||
# 屏蔽默认掉落,改为返还带钉子数的盒子物品
|
||
args["spawnResources"] = False
|
||
self._giveBox(args.get("playerId"), charges)
|