代码初始化

This commit is contained in:
Giao committed 2026-10-09 18:23:57 +08:00
1 parent c2f5cd98ff
commit e3efeb74ad
2439 files changed
+305661 -1

No files matched your search

@@ -0,0 +1,358 @@
# -*- 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)