# -*- 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)