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"; /// Урна-логист (16.10: «Я не могу ставить урны рядом. Может нам для логиста вообще сделать отдельные урны?»): /// без зоны - ставится вплотную к любым урнам, только нити и перевозка, глазницы всегда зелёные. Лимит - общий. public const string LogistBlockName = "necroCinerariumLogist"; public const string LogistSkullBlockName = "necroCinerariumLogistSkull"; /// Логист у стены (18.10, кадило на кронштейне): тот же логист, другой вид - выбор в меню R (CinerariumLogistForm). public const string LogistWallBlockName = "necroCinerariumLogistWall"; public const string LogistWallSkullBlockName = "necroCinerariumLogistWallSkull"; public static bool IsLogistBlock(Block _b) { string n = _b?.GetBlockName(); return n == LogistBlockName || n == LogistSkullBlockName || n == LogistWallBlockName || n == LogistWallSkullBlockName; } public static bool IsLogistWallBlock(Block _b) { string n = _b?.GetBlockName(); return n == LogistWallBlockName || n == LogistWallSkullBlockName; } /// Урна рабочего (17.10, BACKLOG «Урна рабочего»): изготавливает предмет из ячейки образца на станциях, к которым /// от неё тянутся нити; ингредиенты - из ящиков с нитями к урне, готовое - в ящики с нитями от урны. Без зоны и вне лимита, /// как логист. public const string WorkerBlockName = "necroCinerariumWorker"; public const string WorkerSkullBlockName = "necroCinerariumWorkerSkull"; public static bool IsWorkerBlock(Block _b) { string n = _b?.GetBlockName(); return n == WorkerBlockName || n == WorkerSkullBlockName; } /// Урна без зоны (логист, рабочий): ставится вплотную к любым урнам, в лимит не входит, работает по нитям. public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b); 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 = 4; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10) public bool HasSkull; public bool ZoneShown; /// Нити (CinerariumThreads.cs): ящики «откуда» и ящик «куда», главные клетки блоков. Есть нити - урна-логист. public readonly List ThreadFrom = new List(); public bool HasThreadTo; public Vector3i ThreadTo; public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0 || ThreadOut.Count > 0; /// Нити рабочего «от урны»: станции (где изготавливать) и ящики (куда класть готовое), главные клетки блоков. public readonly List ThreadOut = new List(); /// Образец рабочего: что изготавливать; число в стопке - сколько держать в запасе (ящики «куда» и выход станций). /// Лежит в «Содержимом» урны второй ячейкой. public ItemStack Sample = ItemStack.Empty.Clone(); /// Урна рабочего - по блоку, как логист. public bool IsWorker { get { _ = IsLogist; // узнаёт тип блока return _worker; } } bool _worker; public bool IsZoneless => IsLogist || IsWorker; /// Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру. public bool IsLogist { get { if (!_logistKnown) { World w = GameManager.Instance?.World; BlockValue bv = w != null ? w.GetBlock(ToWorldPos()) : BlockValue.Air; if (IsUrn(bv.Block)) { _logist = IsLogistBlock(bv.Block); _worker = IsWorkerBlock(bv.Block); _logistKnown = true; } } return _logist; } } bool _logist, _logistKnown; /// Фильтр логиста: бит на группу CinerariumGroups; по умолчанию все включены. public uint GroupMask = CinerariumGroups.All; public bool GroupOn(int _g) => (GroupMask & (1u << _g)) != 0; public void ToggleGroup(int _g) { GroupMask ^= 1u << _g; SetModified(); Debug.Log("[NecromancerTome] Cinerarium: фильтр урны " + ToWorldPos() + " - группа " + _g + (GroupOn(_g) ? " включена" : " выключена")); } public override void CopyFromInternal(TileEntityComposite _other) { if (_other.TryGetSelfOrFeature(out TEFeatureCinerarium other)) { HasSkull = other.HasSkull; ZoneShown = other.ZoneShown; ThreadFrom.Clear(); ThreadFrom.AddRange(other.ThreadFrom); HasThreadTo = other.HasThreadTo; ThreadTo = other.ThreadTo; ThreadOut.Clear(); ThreadOut.AddRange(other.ThreadOut); Sample = other.Sample.Clone(); GroupMask = other.GroupMask; _threadsDirty = true; } } /// Блок поставлен (OnBlockAdded составного блока; на загрузке чанка не зовётся). public override void OnAdded(Vector3i _blockPos, BlockValue _blockValue) { base.OnAdded(_blockPos, _blockValue); _logist = IsLogistBlock(_blockValue.Block); _worker = IsWorkerBlock(_blockValue.Block); _logistKnown = true; string placed = _blockValue.Block?.GetBlockName(); if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName) { HasSkull = true; // поставили урну, поднятую с черепом } if (ConnectionManager.Instance.IsServer) { CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker); } } /// Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает. public override void OnRemove(World _world) { base.OnRemove(_world); RemoveBounds(); RemoveThreadView(); RemoveGenie(false); CinerariumContents.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 + " убрана - череп выпал"); } } if (Sample != null && !Sample.IsEmpty() && _world != null) { _world.gameManager.ItemDropServer(Sample.Clone(), World.blockToTransformPos(pos) + Vector3.up * 0.7f, Vector3.zero); Sample = ItemStack.Empty.Clone(); Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - образец выпал"); } } public override void OnUnload(World _world) { base.OnUnload(_world); RemoveBounds(); RemoveThreadView(); _threadsDirty = true; // чанк вернётся - нити построить заново RemoveGenie(false); CinerariumContents.CloseIfShowing(this); } public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode) { base.Write(_bw, _eStreamMode); CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown); List t = new List { ThreadFrom.Count }; foreach (Vector3i p in ThreadFrom) { t.Add(p.x); t.Add(p.y); t.Add(p.z); } t.Add(HasThreadTo ? 1 : 0); t.Add(ThreadTo.x); t.Add(ThreadTo.y); t.Add(ThreadTo.z); CinerariumWriteHelper.WriteInts(_bw, t.ToArray()); // Второе число - сколько групп было при записи: новые группы (в конец) у старых урн включаются по правилу ниже. CinerariumWriteHelper.WriteInts(_bw, new[] { unchecked((int)GroupMask), CinerariumGroups.Count }); List o = new List { ThreadOut.Count }; foreach (Vector3i p in ThreadOut) { o.Add(p.x); o.Add(p.y); o.Add(p.z); } CinerariumWriteHelper.WriteInts(_bw, o.ToArray()); (Sample ?? ItemStack.Empty).Clone().Write(_bw); } public override void Read(PooledBinaryReader _br, StreamModeRead _eStreamMode) { base.Read(_br, _eStreamMode); byte version = _br.ReadByte(); HasSkull = _br.ReadBoolean(); ZoneShown = _br.ReadBoolean(); ThreadFrom.Clear(); HasThreadTo = false; if (version >= 2) { int n = _br.ReadInt32(); int[] v = new int[n]; for (int i = 0; i < n; i++) { v[i] = _br.ReadInt32(); } int k = 0; int from = n > 0 ? v[k++] : 0; for (int i = 0; i < from && k + 2 < n; i++, k += 3) { ThreadFrom.Add(new Vector3i(v[k], v[k + 1], v[k + 2])); } if (k + 3 < n) { HasThreadTo = v[k] != 0; ThreadTo = new Vector3i(v[k + 1], v[k + 2], v[k + 3]); } } GroupMask = CinerariumGroups.All; if (version >= 3) { int n = _br.ReadInt32(); int known = 22; // до 17.10 число групп не писалось - их было 22 for (int i = 0; i < n; i++) { int v = _br.ReadInt32(); if (i == 0) { GroupMask = unchecked((uint)v); } else if (i == 1) { known = v; } } // Группы, которых урна ещё не знала: корм кур раньше ехал с «Сырым» - берёт его выбор, прочие новые - включены. for (int g = known; g < CinerariumGroups.Count; g++) { bool on = g == CinerariumGroups.ChickenFeed ? (GroupMask & (1u << CinerariumGroups.Raw)) != 0 : true; GroupMask = on ? GroupMask | (1u << g) : GroupMask & ~(1u << g); } } ThreadOut.Clear(); Sample = ItemStack.Empty.Clone(); if (version >= 4) { int n = _br.ReadInt32(); int[] v = new int[n]; for (int i = 0; i < n; i++) { v[i] = _br.ReadInt32(); } int cnt = n > 0 ? v[0] : 0; for (int i = 0, k = 1; i < cnt && k + 2 < n; i++, k += 3) { ThreadOut.Add(new Vector3i(v[k], v[k + 1], v[k + 2])); } Sample = ItemStack.Empty.Clone().Read(_br); } _threadsDirty = true; if (version > SaveVersion) { Debug.LogWarning("[NecromancerTome] Cinerarium: урна из более новой версии мода (" + version + ") - прочитано только известное"); } } /// Меню как у велосипеда (EntityVehicle.InitLocalActivationCommands): «skull» - «Содержимое» (ячейка черепа; 16.10: «вместо пиктограммы /// багажника... хотя бы череп» - ui_game_symbol_skull, иконка = ui_game_symbol_<имя>, XUiC_Radial), «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", "skull", _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); SyncThreads(_world); Work(_world); } // ------------------------------------------------------------------ // Нити (CinerariumThreads.cs). // ------------------------------------------------------------------ bool _threadsDirty = true; GameObject _threadView; float _nextThreadCheck; public bool AddThreadFrom(Vector3i _box) { if (ThreadFrom.Contains(_box)) { return true; } if (ThreadFrom.Count >= CinerariumThreads.MaxFrom) { return false; } ThreadFrom.Add(_box); ThreadsChanged(); return true; } public void SetThreadTo(Vector3i _box) { HasThreadTo = true; ThreadTo = _box; ThreadsChanged(); } public bool AddThreadOut(Vector3i _target) { if (ThreadOut.Contains(_target)) { return true; } if (ThreadOut.Count >= CinerariumThreads.MaxFrom) { return false; } ThreadOut.Add(_target); ThreadsChanged(); return true; } public int ClearThreads() { int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0) + ThreadOut.Count; ThreadFrom.Clear(); ThreadOut.Clear(); HasThreadTo = false; if (n > 0) { ThreadsChanged(); } return n; } public int RemoveThreadsTo(Vector3i _box) { int n = ThreadFrom.RemoveAll(p => p == _box) + ThreadOut.RemoveAll(p => p == _box); if (HasThreadTo && ThreadTo == _box) { HasThreadTo = false; n++; } if (n > 0) { ThreadsChanged(); } return n; } void ThreadsChanged() { _threadsDirty = true; _glowState = -1; // логист - другие глаза SetModified(); Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет") + (ThreadOut.Count > 0 ? ", рабочему " + ThreadOut.Count : "")); } /// Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений. void SyncThreads(World _world) { if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsZoneless && _logistKnown) { Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у логиста и рабочего"); ThreadFrom.Clear(); ThreadOut.Clear(); HasThreadTo = false; ThreadsChanged(); } if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && Time.time >= _nextThreadCheck) { _nextThreadCheck = Time.time + 2f; bool gone = false; foreach (Vector3i p in ThreadFrom.ToArray()) { if (!CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u) || u != null) { if (_world.GetChunkFromWorldPos(p) != null) // чанк не загружен - не снимать { ThreadFrom.Remove(p); gone = true; } } } if (HasThreadTo && (!CinerariumThreads.Resolve(_world, ThreadTo, out Vector3i _, out TEFeatureCinerarium u2) || u2 != null) && _world.GetChunkFromWorldPos(ThreadTo) != null) { HasThreadTo = false; gone = true; } foreach (Vector3i p in ThreadOut.ToArray()) { bool box = CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u3) && u3 == null; if (!box && CinerariumThreads.StationAt(_world, p) == null && _world.GetChunkFromWorldPos(p) != null) { ThreadOut.Remove(p); gone = true; } } if (gone) { ThreadsChanged(); } } if (!_threadsDirty && (_threadView != null || !HasThreads)) { return; } Transform t = ModelTransform(); if (t == null) { return; } _threadsDirty = false; SyncGlow(); if (!HasThreads) { RemoveThreadView(); return; } if (_threadView == null) { _threadView = new GameObject("NecroCinerariumThreads"); _threadView.transform.SetParent(t, false); _threadView.AddComponent(); } Vector3i me = ToWorldPos(); List<(Vector3 a, Vector3 b)> pairs = new List<(Vector3, Vector3)>(); foreach (Vector3i p in ThreadFrom) { pairs.Add((CinerariumThreads.BoxPoint(p, me), CinerariumThreads.UrnPoint(me))); } if (HasThreadTo) { pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me))); } foreach (Vector3i p in ThreadOut) { pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(p, me))); } _threadView.GetComponent().Build(pairs); } void RemoveThreadView() { if (_threadView != null) { UnityEngine.Object.Destroy(_threadView); _threadView = null; } } float _nextWork; public bool Stuck; /// Дело найдено пробным шагом, дух полетел; выполнить в _actAt (16.10: «делал действие не сразу»). bool _pending; float _actAt; /// Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере. /// Сначала пробный шаг (ничего не меняет) - куда лететь; само дело - когда дух долетел и машет руками. Если за это /// время что-то поменялось, настоящий шаг сделает то, что нужно на тот момент. void Work(World _world) { if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer) { _pending = false; return; } CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent() : null; if (_pending) { if (Time.time < _actAt) { return; } _pending = false; CinerariumWork.Step(_world, this, out bool _, false); _nextWork = Time.time + CinerariumWork.StepSeconds; return; } if (Time.time < _nextWork) { return; } _nextWork = Time.time + CinerariumWork.StepSeconds; Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck, true); if (stuck != Stuck) { Stuck = stuck; _glowState = -1; // логист - мигание Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? (IsLogist ? " застрял - ящик «куда» полон, урна мигает" : " почернел - готовое класть некуда") : " ожил")); } if (motion != null && !IsLogist) { motion.SetStuck(Stuck && at == null); } if (at.HasValue) { _pending = true; _actAt = Time.time; if (motion != null) { Vector3i? from = CinerariumWork.LastFrom; List<(Vector3, Vector3, bool)> stops = new List<(Vector3, Vector3, bool)>(); if (from.HasValue) { stops.Add((Stand(_world, from.Value, out bool b0) - Origin.position, World.blockToTransformPos(from.Value) - Origin.position, b0)); } stops.Add((Stand(_world, at.Value, out bool beside) - Origin.position, World.blockToTransformPos(at.Value) - Origin.position, beside)); _actAt = Time.time + motion.Visit(stops); } } } /// Где духу встать у места работы (16.10: «влетает в станцию в самый центр... логичнее к краю станции с /// доступной стороны»): из четырёх соседних клеток - свободная (воздух или растение), ближайшая к урне; дух встаёт у /// её края со стороны места. Свободной нет - над местом. Возвращает низ точки в координатах мира (blockToTransformPos). Vector3 Stand(World _world, Vector3i _at, out bool _beside) { Vector3i urn = ToWorldPos(); Vector3i best = _at; int bestD = int.MaxValue; foreach (Vector3i dir in new[] { Vector3i.forward, Vector3i.back, Vector3i.left, Vector3i.right }) { Vector3i n = _at + dir; BlockValue bv = _world.GetBlock(n); if (!bv.isair && !(bv.Block is BlockPlant)) { continue; } int d = (n.x - urn.x) * (n.x - urn.x) + (n.z - urn.z) * (n.z - urn.z); if (d < bestD) { bestD = d; best = n; } } _beside = best != _at; Vector3 at = World.blockToTransformPos(_at); if (!_beside) { return at; } Vector3 n0 = World.blockToTransformPos(best); return Vector3.Lerp(n0, at, 0.2f); // чуть ближе к месту - у самого края } // ------------------------------------------------------------------ // Череп. // ------------------------------------------------------------------ /// Череп положен в ячейку багажника или вынут из неё. public void SetSkull(bool _has) { if (HasSkull == _has) { return; } HasSkull = _has; SetModified(); SyncGenie(true); Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут")); } /// Образец рабочего положен, изменён или вынут. public void SetSample(ItemStack _s) { ItemStack was = Sample ?? ItemStack.Empty; if (was.itemValue.type == _s.itemValue.type && was.count == _s.count) { return; } Sample = _s; SetModified(); Debug.Log("[NecromancerTome] Cinerarium: образец урны " + ToWorldPos() + " - " + (_s.IsEmpty() ? "вынут" : _s.itemValue.ItemClass?.GetItemName() + " x" + _s.count)); } /// «Починить», как у транспорта: целиком, за 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) { int logist = 0; if (IsLogist) { World w = GameManager.Instance?.World; logist = w != null && IsLogistWallBlock(w.GetBlock(ToWorldPos()).Block) ? 2 : 1; } _genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f, logist); 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(); // Логист (18.10, кадило): своё свечение в материале - внутренность кадила, светятся окна крышки. Застрял (класть // некуда) - вместо почерневшего духа мигает (18.10: «При застревании должно мигать»): CinerariumBlink на модели. bool blink = HasSkull && IsLogist && Stuck; int want = (HasSkull ? 1 : 0) + (blink ? 2 : 0); if (t == null || (_glowState == want && _glowModel == t)) { return; } List own = new List(); foreach (Renderer r in t.GetComponentsInChildren(true)) { if (_genie != null && r.transform.IsChildOf(_genie.transform) || _bounds != null && r.transform.IsChildOf(_bounds.transform) || _threadView != null && r.transform.IsChildOf(_threadView.transform)) { continue; } own.Add(r); r.GetPropertyBlock(_glowBlock); _glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black); r.SetPropertyBlock(_glowBlock); } CinerariumBlink b = t.GetComponent(); if (blink && b == null) { b = t.gameObject.AddComponent(); } if (b != null) { b.Renderers = own; b.enabled = blink; } _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 || n == LogistBlockName || n == LogistSkullBlockName || n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName; } 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, Block _placing) { if (!IsZonelessBlock(_placing) && CinerariumRegistry.Overlaps(_pos)) { return "necroCinerariumOverlap"; } if (IsZonelessBlock(_placing)) { return null; // логисты и рабочие в лимит не входят (16.10, 17.10 - пользователь) } 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; /// Логисты (третье поле строки «L»): в лимит не входят. static HashSet _logists = new HashSet(); 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(); _logists = new HashSet(); 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] : ""; if (parts.Length > 2 && parts[2] == "L") { _logists.Add(new Vector3i(x, y, z)); } } } } } 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); if (_logists.Contains(kv.Key)) { sb.Append("|L"); } sb.Append('\n'); } File.WriteAllText(FilePath, sb.ToString()); } catch (Exception e) { Debug.LogWarning("[NecromancerTome] Cinerarium: реестр урн не записан (" + e.Message + ")"); } } public static void Add(Vector3i _pos, PlatformUserIdentifierAbs _owner, bool _logist) { Urns()[_pos] = _owner != null ? _owner.CombinedString : ""; if (_logist) { _logists.Add(_pos); } else { _logists.Remove(_pos); } Save(); Debug.Log("[NecromancerTome] Cinerarium: урна поставлена " + _pos + ", всего в сейве " + _urns.Count); } public static void Remove(Vector3i _pos) { if (Urns().Remove(_pos)) { _logists.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; World world = GameManager.Instance?.World; foreach (KeyValuePair kv in Urns()) { // Логисты не считаются; записанные до метки «L» узнаются по блоку в мире (чанк рядом загружен). if (kv.Value == id && !_logists.Contains(kv.Key) && !(world != null && TEFeatureCinerarium.IsZonelessBlock(world.GetBlock(kv.Key).Block))) { n++; } } return n; } public static List All() { return new List(Urns().Keys); } /// Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям, /// по высоте ближе 6 (зона от -1 до +4). public static bool Overlaps(Vector3i _pos) { int w = TEFeatureCinerarium.HalfWidth * 2; int h = TEFeatureCinerarium.Below + TEFeatureCinerarium.Above; World world = GameManager.Instance?.World; foreach (Vector3i p in Urns().Keys) { if (world != null && TEFeatureCinerarium.IsZonelessBlock(world.GetBlock(p).Block)) { continue; // у логиста и рабочего зоны нет } 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")) { // Как у транспорта (16.10: «почему нету значка Забрать с рукой?»): игра даёт «Забрать» станциям только внутри // своего земельного камня (TEFeaturePickup -> World.IsMyLandProtectedBlock), транспорт же поднимается где // угодно. Урне - как транспорту: везде, кроме зоны торговца, только хозяину и только с пустой сумкой; // череп (модификация) остаётся в урне. World w = _world as World; bool bagEmpty = !__instance.TryGetSelfOrFeature(out TEFeatureStorage bag) || bag.IsEmpty(); bool mine = __instance.Owner == null || __instance.LocalPlayerIsOwner; _commands[i].enabled = bagEmpty && mine && (w == null || !w.IsWithinTraderArea(_blockPos)); } else if (text.EndsWith("bounds_show")) { _commands[i].enabled = !urn.ZoneShown && !urn.IsZoneless; } else if (text.EndsWith("bounds_hide")) { _commands[i].enabled = urn.ZoneShown && !urn.IsZoneless; } } } } [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) { CinerariumContents.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, __instance); 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() || urn.Sample != null && !urn.Sample.IsEmpty()) { GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied"); return false; } bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block); bool wall = TEFeatureCinerarium.IsLogistWallBlock(bv.Block); bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block); ItemValue iv = ItemClass.GetItem(wall ? (urn.HasSkull ? TEFeatureCinerarium.LogistWallSkullBlockName : TEFeatureCinerarium.LogistWallBlockName) : logist ? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName) : worker ? (urn.HasSkull ? TEFeatureCinerarium.WorkerSkullBlockName : TEFeatureCinerarium.WorkerBlockName) : (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; } } }