Files
2026-10-09 18:23:57 +08:00

359 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)