using System; using System.Collections.Generic; using System.IO; using System.Text; using HarmonyLib; using Platform; using UnityEngine; namespace NecromancerTome { /// /// Урна-цинерарий (Harvest Cinerarium), этап 2 некротехнолога (BACKLOG «Некротехнолог v2», решения 06.10, /// 15.10, 16.10). Составной блок игры, как пирамида и земельный камень: TEFeatureStorage игры - сумка духа /// (обычное окно хранилища), эта деталь - череп, зона и меню на E. Решение 16.10 «урна как транспорт»: на E /// круговое меню, как у велосипеда - «Багажник» (ячейка черепа, CinerariumTrunk.cs), сумка (хранилище игры), /// зона. Есть ли в урне череп - поле детали с сохранением (CinerariumWriteHelper, версия первым байтом). /// /// Поднятая урна держит череп, как транспорт - модификации: игра при подъёме данных блока в предмет не /// переносит (Block.TakeItemWithTimerDone -> BlockValue.ToItemValue, только тип), поэтому два блока - /// necroCinerarium (пустая) и necroCinerariumSkull (с черепом); подъём отдаёт тот, что соответствует черепу, /// постановка второго кладёт череп сразу. /// /// Зона работника - 5x5 по горизонтали с урной в центре, по высоте от -1 до +4 блока (06.10). Зоны урн не /// пересекаются, урн у игрока не больше ceil(убийств / 500) - обе проверки при постановке /// (CanPlaceBlockAt), по реестру урн сейва (CinerariumRegistry). /// /// Имена команд не должны кончаться на zone_show/zone_hide/effect_on/effect_off: заплатка пирамиды на /// BlockCompositeTileEntity.OnBlockActivated ловит их по EndsWith у ЛЮБОГО составного блока. /// public class TEFeatureCinerarium : TEFeatureAbs { public const string BlockName = "necroCinerarium"; public const string SkullBlockName = "necroCinerariumSkull"; public const string SkullItemName = "necroSkull"; public const int HalfWidth = 2; public const int Below = 1; public const int Above = 4; public const float KillsPerUrn = 500f; const byte SaveVersion = 1; public bool HasSkull; public bool ZoneShown; public override void CopyFromInternal(TileEntityComposite _other) { if (_other.TryGetSelfOrFeature(out TEFeatureCinerarium other)) { HasSkull = other.HasSkull; ZoneShown = other.ZoneShown; } } /// Блок поставлен (OnBlockAdded составного блока; на загрузке чанка не зовётся). public override void OnAdded(Vector3i _blockPos, BlockValue _blockValue) { base.OnAdded(_blockPos, _blockValue); if (_blockValue.Block?.GetBlockName() == SkullBlockName) { HasSkull = true; // поставили урну, поднятую с черепом } if (ConnectionManager.Instance.IsServer) { CinerariumRegistry.Add(_blockPos, Parent?.Owner); } } /// Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает. public override void OnRemove(World _world) { base.OnRemove(_world); RemoveBounds(); RemoveGenie(false); XUiC_NecroCinerariumSkullGrid.CloseIfShowing(this); if (!ConnectionManager.Instance.IsServer) { return; } Vector3i pos = ToWorldPos(); CinerariumRegistry.Remove(pos); // Подняли целиком - череп уехал вместе с урной (necroCinerariumSkull), не выбрасывать. bool pickedUp = Patch_Block_TakeItemWithTimerDone_Cinerarium.WasPickedUp(pos); if (HasSkull && _world != null && !pickedUp) { HasSkull = false; ItemValue skull = ItemClass.GetItem(SkullItemName); if (skull != null && skull.type > 0) { _world.gameManager.ItemDropServer(new ItemStack(skull, 1), World.blockToTransformPos(pos) + Vector3.up * 0.7f, Vector3.zero); Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - череп выпал"); } } } public override void OnUnload(World _world) { base.OnUnload(_world); RemoveBounds(); RemoveGenie(false); XUiC_NecroCinerariumSkullGrid.CloseIfShowing(this); } public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode) { base.Write(_bw, _eStreamMode); CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown); } public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode) { base.Read(_br, _eStreamMode); byte version = _br.ReadByte(); HasSkull = _br.ReadBoolean(); ZoneShown = _br.ReadBoolean(); if (version > SaveVersion) { Debug.LogWarning("[NecromancerTome] Cinerarium: урна из более новой версии мода (" + version + ") - прочитано только известное"); } } /// Меню как у велосипеда (EntityVehicle.InitLocalActivationCommands): «service» - багажник (у транспорта /// «Обслуживание», окно модификаций), «wrench» - починить, «frames» - границы, как у земельного камня. Сумку даёт /// TEFeatureStorage, подъём - TEFeaturePickup («Take»), обе детали игры. «Сумка» (16.10: «переименуй в Сумка») - /// своя команда с иконкой мешка, как «Хранилище» у транспорта: открывает то же хранилище командой /// TEFeatureStorage:Search (составной блок выполняет команду по имени, не глядя на enabled), а «Обыскать» игры у /// урны спрятан. Стоит первой - короткое E открывает сумку. public override void InitBlockActivationCommands(Action _addCallback) { base.InitBlockActivationCommands(_addCallback); _addCallback(new BlockActivationCommand("bag", "loot_sack", _enabled: false), TileEntityComposite.EBlockCommandOrder.First, FeatureData); _addCallback(new BlockActivationCommand("trunk", "service", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); _addCallback(new BlockActivationCommand("urn_repair", "wrench", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); _addCallback(new BlockActivationCommand("bounds_show", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); _addCallback(new BlockActivationCommand("bounds_hide", "frames", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData); } public override string GetActivationText(WorldBase _world, Vector3i _blockPos, BlockValue _blockValue, EntityAlive _entityFocusing, string _activateHotkeyMarkup, string _focusedTileEntityName) { base.GetActivationText(_world, _blockPos, _blockValue, _entityFocusing, _activateHotkeyMarkup, _focusedTileEntityName); return _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " + Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty"); } public override void UpdateTick(World _world) { base.UpdateTick(_world); if (ZoneShown && _bounds == null) { SpawnBounds(); } SyncGenie(false); Work(_world); } float _nextWork; public bool Stuck; /// Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере. void Work(World _world) { if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer || Time.time < _nextWork) { return; } _nextWork = Time.time + CinerariumWork.StepSeconds; Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck); if (stuck != Stuck) { Stuck = stuck; Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? " почернел - готовое класть некуда" : " ожил")); } CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null; if (motion != null) { motion.SetStuck(Stuck && at == null); if (at.HasValue) { motion.Visit(World.blockToTransformPos(at.Value) - Origin.position); } } } // ------------------------------------------------------------------ // Череп. // ------------------------------------------------------------------ /// Череп положен в ячейку багажника или вынут из неё. public void SetSkull(bool _has) { if (HasSkull == _has) { return; } HasSkull = _has; SetModified(); SyncGenie(true); Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут")); } /// «Починить», как у транспорта: целиком, за RepairBones костей из рюкзака (сколько берёт и молоток - /// RepairItems в blocks.xml). public const string RepairItemName = "resourceBone"; public const int RepairBones = 5; public static int BonesCarried(EntityPlayerLocal _player) { ItemValue bone = ItemClass.GetItem(RepairItemName); if (_player == null || bone == null || bone.type <= 0) { return 0; } return (_player.inventory?.GetItemCount(bone) ?? 0) + (_player.bag?.GetItemCount(bone) ?? 0); } public static bool Repair(World _world, Vector3i _pos, EntityPlayerLocal _player) { BlockValue bv = _world.GetBlock(_pos); ItemValue bone = ItemClass.GetItem(RepairItemName); if (bv.damage <= 0 || bone == null || bone.type <= 0 || BonesCarried(_player) < RepairBones) { return false; } int need = RepairBones; if (_player.bag != null) { need -= _player.bag.DecItem(bone, need); } if (need > 0 && _player.inventory != null) { need -= _player.inventory.DecItem(bone, need); } bv.damage = 0; _world.SetBlockRPC(_pos, bv); _player.PlayOneShot("crafting/craft_repair_item", true); Debug.Log("[NecromancerTome] Cinerarium: урна " + _pos + " починена"); return true; } // ------------------------------------------------------------------ // Джинн из урны (CinerariumGenie.cs): есть череп - есть дух. // ------------------------------------------------------------------ GameObject _genie; public const string GenieSound = "necroTechGust"; /// _sound - череп только что положили или вынули (порыв); при загрузке чанка дух появляется молча. public void SyncGenie(bool _sound) { SyncGlow(); if (HasSkull && _genie == null) { _genie = CinerariumGenie.Spawn(ModelTransform()); if (_genie != null && _sound) { Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound); } } else if (!HasSkull && _genie != null) { RemoveGenie(_sound); } } /// Руны кровью и глазницы черепов светятся, только когда в урне череп (16.10): _EmissionColor через /// MaterialPropertyBlock на рендерерах модели урны (материал общий не трогается), духа не задевает. public static Color GlowColor = new Color(2.2f, 2.2f, 2.2f, 1f); static readonly MaterialPropertyBlock _glowBlock = new MaterialPropertyBlock(); static readonly int EmissionColorId = Shader.PropertyToID("_EmissionColor"); int _glowState = -1; // -1 - не ставили на эту модель void SyncGlow() { Transform t = ModelTransform(); int want = HasSkull ? 1 : 0; if (t == null || (_glowState == want && _glowModel == t)) { return; } foreach (Renderer r in t.GetComponentsInChildren(true)) { if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform)) { continue; } r.GetPropertyBlock(_glowBlock); _glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black); r.SetPropertyBlock(_glowBlock); } _glowState = want; _glowModel = t; } Transform _glowModel; void RemoveGenie(bool _sound) { if (_genie == null) { return; } UnityEngine.Object.Destroy(_genie); _genie = null; if (_sound) { Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound); } } // ------------------------------------------------------------------ // Границы зоны: каркас параллелепипеда, как у земельного камня. Дочерний объект модели блока - сдвиги // Origin и выгрузка чанка его не теряют (как кольцо пирамиды); материал один на все урны. // ------------------------------------------------------------------ GameObject _bounds; static Material _boundsMat; Transform ModelTransform() { World world = GameManager.Instance?.World; if (world == null) { return null; } Vector3i pos = ToWorldPos(); Chunk chunk = world.GetChunkFromWorldPos(pos) as Chunk; BlockEntityData be = chunk?.GetBlockEntity(pos); return be != null && be.bHasTransform ? be.transform : null; } public void SpawnBounds() { if (_bounds != null) { return; } Transform t = ModelTransform(); if (t == null) { return; } _bounds = new GameObject("NecroCinerariumBounds"); _bounds.transform.SetParent(t, false); LineRenderer lr = _bounds.AddComponent(); lr.useWorldSpace = false; lr.widthMultiplier = 0.06f; if (_boundsMat == null) { _boundsMat = new Material(Shader.Find("Sprites/Default")) { name = "NecroCinerariumBounds" }; } lr.sharedMaterial = _boundsMat; Color col = new Color(0.55f, 0.9f, 0.75f, 0.85f); lr.startColor = col; lr.endColor = col; lr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; lr.receiveShadows = false; // Углы по граням блоков: x и z от -2.5 до +2.5 от центра урны, y от низа блока под ней до верха +4. Vector3 c = World.blockToTransformPos(ToWorldPos()) - Origin.position; float x0 = c.x - HalfWidth - 0.5f, x1 = c.x + HalfWidth + 0.5f; float z0 = c.z - HalfWidth - 0.5f, z1 = c.z + HalfWidth + 0.5f; float y0 = c.y - Below, y1 = c.y + Above + 1f; Vector3 b0 = new Vector3(x0, y0, z0), b1 = new Vector3(x1, y0, z0), b2 = new Vector3(x1, y0, z1), b3 = new Vector3(x0, y0, z1); Vector3 t0 = new Vector3(x0, y1, z0), t1 = new Vector3(x1, y1, z0), t2 = new Vector3(x1, y1, z1), t3 = new Vector3(x0, y1, z1); // Одна ломаная по всем 12 рёбрам (часть рёбер проходится дважды - на глаз не видно). Vector3[] path = { b0, b1, b2, b3, b0, t0, t1, b1, t1, t2, b2, t2, t3, b3, t3, t0 }; lr.positionCount = path.Length; for (int i = 0; i < path.Length; i++) { lr.SetPosition(i, t.InverseTransformPoint(path[i])); } } public void RemoveBounds() { if (_bounds != null) { UnityEngine.Object.Destroy(_bounds); _bounds = null; } } // ------------------------------------------------------------------ // Постановка: лимит и пересечение зон. // ------------------------------------------------------------------ public static bool IsUrn(Block _b) { string n = _b?.GetBlockName(); return n == BlockName || n == SkullBlockName; } public static int UrnLimit(EntityPlayer _player) { float kills = _player?.Buffs != null ? _player.Buffs.GetCustomVar(Patch_EntityPlayer_AddKillXP_NecromancyCount.KillsCVarName) : 0f; return Mathf.CeilToInt(kills / KillsPerUrn); } /// Ключ подсказки, почему урну сюда нельзя, или null. public static string PlacementRefusal(Vector3i _pos) { if (CinerariumRegistry.Overlaps(_pos)) { return "necroCinerariumOverlap"; } World world = GameManager.Instance?.World; EntityPlayerLocal player = world?.GetPrimaryPlayer(); // Реестр живёт на сервере; клиент в сети лимит не проверяет (мультиплеер поддержан не полностью). if (player != null && world != null && !world.IsRemote()) { PlatformUserIdentifierAbs me = PlatformManager.InternalLocalUserIdentifier; if (CinerariumRegistry.CountFor(me) >= UrnLimit(player)) { return "necroCinerariumLimit"; } } return null; } } /// Все урны сейва: позиция и владелец, файл в папке сейва (как сумка волка). Сервер. public static class CinerariumRegistry { const string FileName = "NecroCinerariums.txt"; static Dictionary _urns; static string _dir; static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName); static Dictionary Urns() { string dir = GameIO.GetSaveGameDir(); if (_urns != null && _dir == dir) { return _urns; } _dir = dir; _urns = new Dictionary(); try { if (File.Exists(FilePath)) { foreach (string line in File.ReadAllLines(FilePath)) { string[] parts = line.Split('|'); string[] xyz = parts[0].Split(','); if (xyz.Length == 3 && int.TryParse(xyz[0], out int x) && int.TryParse(xyz[1], out int y) && int.TryParse(xyz[2], out int z)) { _urns[new Vector3i(x, y, z)] = parts.Length > 1 ? parts[1] : ""; } } } } catch (Exception e) { Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не прочитан (" + e.Message + ")"); } return _urns; } static void Save() { try { StringBuilder sb = new StringBuilder(); foreach (KeyValuePair kv in Urns()) { sb.Append(kv.Key.x).Append(',').Append(kv.Key.y).Append(',').Append(kv.Key.z).Append('|').Append(kv.Value).Append('\n'); } File.WriteAllText(FilePath, sb.ToString()); } catch (Exception e) { Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не записан (" + e.Message + ")"); } } public static void Add(Vector3i _pos, PlatformUserIdentifierAbs _owner) { Urns()[_pos] = _owner != null ? _owner.CombinedString : ""; Save(); Debug.Log("[NecromancerTome] Cinerarium: урна поставлена " + _pos + ", всего в сейве " + _urns.Count); } public static void Remove(Vector3i _pos) { if (Urns().Remove(_pos)) { Save(); Debug.Log("[NecromancerTome] Cinerarium: урна убрана " + _pos + ", всего в сейве " + _urns.Count); } } public static int CountFor(PlatformUserIdentifierAbs _owner) { string id = _owner != null ? _owner.CombinedString : ""; int n = 0; foreach (string v in Urns().Values) { if (v == id) { n++; } } return n; } /// Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям, /// по высоте ближе 6 (зона от -1 до +4). public static bool Overlaps(Vector3i _pos) { int w = TEFeatureCinerarium.HalfWidth * 2; int h = TEFeatureCinerarium.Below + TEFeatureCinerarium.Above; foreach (Vector3i p in Urns().Keys) { if (p != _pos && Math.Abs(p.x - _pos.x) <= w && Math.Abs(p.z - _pos.z) <= w && Math.Abs(p.y - _pos.y) <= h) { return true; } } return false; } } /// Какие команды меню урны доступны сейчас (текст команд общий на тип блока - см. TEFeaturePyramidWard). [HarmonyPatch(typeof(TileEntityComposite), "UpdateBlockActivationCommands")] public static class Patch_TileEntityComposite_UpdateBlockActivationCommands_Cinerarium { public static void Postfix(TileEntityComposite __instance, BlockActivationCommand[] _commands, WorldBase _world, Vector3i _blockPos) { if (__instance == null || _commands == null || !__instance.TryGetSelfOrFeature(out TEFeatureCinerarium urn)) { return; } for (int i = 0; i < _commands.Length; i++) { string text = _commands[i].text; if (string.IsNullOrEmpty(text)) { continue; } if (text.EndsWith(":trunk") || text == "trunk" || text.EndsWith(":bag")) { _commands[i].enabled = true; } else if (text == "TEFeatureStorage:Search") { _commands[i].enabled = false; // вместо него - «Сумка» } else if (text.EndsWith("urn_repair")) { _commands[i].enabled = _world != null && _world.GetBlock(_blockPos).damage > 0; } else if (text.EndsWith(":Take")) { // Как у транспорта: поднять можно, только вынув вещи из сумки; череп (модификация) остаётся в урне. if (__instance.TryGetSelfOrFeature(out TEFeatureStorage bag) && !bag.IsEmpty()) { _commands[i].enabled = false; } } else if (text.EndsWith("bounds_show")) { _commands[i].enabled = !urn.ZoneShown; } else if (text.EndsWith("bounds_hide")) { _commands[i].enabled = urn.ZoneShown; } } } } [HarmonyPatch(typeof(BlockCompositeTileEntity), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })] public static class Patch_BlockCompositeTileEntity_OnBlockActivated_Cinerarium { public static bool Prefix(string _commandName, Vector3i _blockPos, EntityPlayerLocal _player, ref bool __result) { if (string.IsNullOrEmpty(_commandName)) { return true; } bool skull = _commandName.EndsWith(":trunk") || _commandName == "trunk"; bool repair = _commandName.EndsWith("urn_repair"); bool bag = _commandName.EndsWith("TEFeatureCinerarium:bag"); bool bounds = _commandName.EndsWith("bounds_show") || _commandName.EndsWith("bounds_hide"); if (!skull && !bounds && !repair && !bag) { return true; } World world = GameManager.Instance?.World; if (!(world?.GetTileEntity(_blockPos) is TileEntityComposite composite) || !composite.TryGetSelfOrFeature(out TEFeatureCinerarium urn)) { return true; } if (bag) { __result = composite.OnBlockActivated(null, "TEFeatureStorage:Search", world, _blockPos, world.GetBlock(_blockPos), _player); return false; } if (skull) { XUiC_NecroCinerariumSkullGrid.Open(_player, urn); } else if (repair) { if (!TEFeatureCinerarium.Repair(world, _blockPos, _player)) { GameManager.ShowTooltip(_player, "necroCinerariumNoBones"); } } else { urn.ZoneShown = !urn.ZoneShown; if (urn.ZoneShown) { urn.SpawnBounds(); } else { urn.RemoveBounds(); } urn.SetModified(); } __result = true; return false; } } /// Лимит урн и пересечение зон. Составной блок CanPlaceBlockAt не переопределяет - заплатка на Block, /// с отбором по имени блока. Зовётся и каждый кадр для призрака постановки (RenderDisplacedCube) - он /// краснеет; причину показывает подмена подсказки «нельзя поставить» при щелчке (ниже). [HarmonyPatch(typeof(Block), nameof(Block.CanPlaceBlockAt))] public static class Patch_Block_CanPlaceBlockAt_Cinerarium { public static string LastRefusal; public static float LastRefusalTime; public static void Postfix(Block __instance, Vector3i _blockPos, ref bool __result) { if (!__result || __instance == null || !TEFeatureCinerarium.IsUrn(__instance)) { return; } string why = TEFeatureCinerarium.PlacementRefusal(_blockPos); if (why != null) { __result = false; LastRefusal = why; LastRefusalTime = Time.time; } } } [HarmonyPatch(typeof(GameManager), nameof(GameManager.ShowTooltip), new Type[] { typeof(EntityPlayerLocal), typeof(string), typeof(bool), typeof(bool), typeof(float) })] public static class Patch_GameManager_ShowTooltip_Cinerarium { public static void Prefix(ref string _text) { if (_text == "blockCantPlaced" && Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal != null && Time.time - Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusalTime < 0.1f) { _text = Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal; Patch_Block_CanPlaceBlockAt_Cinerarium.LastRefusal = null; } } } /// Подъём урны («Take» детали TEFeaturePickup -> Block.TakeItemWithTimer): по окончании таймера игра отдаёт /// предмет по типу блока (BlockValue.ToItemValue), OnBlockPickedUp не зовёт. Для урны - своя копия того же метода: /// с черепом - предмет necroCinerariumSkull, без - necroCinerarium; сумка за время таймера не должна наполниться. /// Позиция запоминается - OnRemove урны по ней понимает, что её подняли, а не разбили (череп не выбрасывать). [HarmonyPatch(typeof(Block), nameof(Block.TakeItemWithTimerDone))] public static class Patch_Block_TakeItemWithTimerDone_Cinerarium { static Vector3i _pos; static float _time = -100f; public static bool WasPickedUp(Vector3i _p) => _p == _pos && Time.time - _time < 5f; public static bool Prefix(TimerEventData _timerData) { World world = GameManager.Instance?.World; if (world == null || !(_timerData?.Data is object[] obj) || obj.Length < 3 || !(obj[1] is Vector3i pos) || !(obj[0] is BlockValue started) || !(obj[2] is EntityPlayerLocal player)) { return true; } BlockValue bv = world.GetBlock(pos); if (!TEFeatureCinerarium.IsUrn(bv.Block) || bv.type != started.type || bv.damage > 0 || !(world.GetTileEntity(pos) is TileEntityComposite composite) || !composite.TryGetSelfOrFeature(out TEFeatureCinerarium urn)) { return true; // чужой блок или отказ игры (повреждён, подменён) - пусть игра сама скажет почему } if (composite.IsUserAccessing()) { GameManager.ShowTooltip(player, Localization.Get("ttCantPickupInUse"), string.Empty, "ui_denied"); return false; } if (composite.TryGetSelfOrFeature(out TEFeatureStorage bag) && !bag.IsEmpty()) { GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied"); return false; } ItemValue iv = ItemClass.GetItem(urn.HasSkull ? TEFeatureCinerarium.SkullBlockName : TEFeatureCinerarium.BlockName); if (iv == null || iv.type <= 0) { return true; } _pos = pos; _time = Time.time; ItemStack stack = new ItemStack(iv, 1); LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(player); if (!ui.xui.PlayerInventory.AddItem(stack)) { ui.xui.PlayerInventory.DropItem(stack); } world.SetBlockRPC(pos, BlockValue.Air); Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " поднята" + (urn.HasSkull ? " с черепом" : "")); return false; } } }