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 partial 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;
}
/// Урна без зоны 5x5 (логист, рабочий, ремонтник, лутер): ставится вплотную к любым урнам, в лимит не входит,
/// работает по нитям. У ремонтника и лутера своя большая зона (CinerariumZone.cs) - зоны таких урн могут пересекаться.
public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b) || IsRepairBlock(_b) || IsLootBlock(_b) || IsProspectorBlock(_b);
public const string SkullItemName = "necroSkull";
public const int HalfWidth = 2;
public const int Below = 1;
public const int Above = 4;
/// Убийств на одну урну сборщика. 17.10: «лимит урн - ослабить/убрать» - нагрузку теперь держит общий бюджет
/// шагов (CinerariumLoad), лимит остался мягким игровым порогом: было 500, стало 200.
public const float KillsPerUrn = 50f; // 18.10 вечером: «Пусть будет 50 киллов на урну» (было 200, до того 500)
const byte SaveVersion = 6; // 2 - нити, 3 - фильтр логиста (16.10), 4 - нити рабочего «куда» и образец (18.10), 5 - черепа-ретрансляторы (18.10), 6 - зона и запуск ремонтника/лутера (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();
/// Черепа-ретрансляторы на пути каждой нити (18.10: «сам череп повысить из простого предмета, до промежуточного
/// ретранслятора... если понадобится протащить коммуникации дальше»; «Цепочка из нескольких»): клетки черепов-декора по
/// порядку ОТ УРНЫ к концу нити. Списки идут вровень с ThreadFrom / ThreadOut, ToVia - у нити «куда». Пусто - нить прямая.
/// Каждый пролёт - не длиннее CinerariumThreads.MaxLength. Убрали череп - нить через него снимается.
public readonly List> FromVia = new List>();
public readonly List> OutVia = new List>();
public readonly List ToVia = new List();
/// Образец рабочего: что изготавливать; число в стопке - сколько держать в запасе (ящики «куда» и выход станций).
/// Лежит в «Содержимом» урны второй ячейкой.
public ItemStack Sample = ItemStack.Empty.Clone();
/// Урна рабочего - по блоку, как логист.
public bool IsWorker
{
get
{
_ = IsLogist; // узнаёт тип блока
return _worker;
}
}
bool _worker;
public bool IsZoneless => IsLogist || IsWorker || IsBigZone || IsProspector;
/// Урна-логист - по блоку (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);
_repair = IsRepairBlock(bv.Block);
_loot = IsLootBlock(bv.Block);
_prospector = IsProspectorBlock(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);
CopyVia(other.FromVia, FromVia);
CopyVia(other.OutVia, OutVia);
ToVia.Clear();
ToVia.AddRange(other.ToVia);
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);
_repair = IsRepairBlock(_blockValue.Block);
_loot = IsLootBlock(_blockValue.Block);
_prospector = IsProspectorBlock(_blockValue.Block);
_logistKnown = true;
string placed = _blockValue.Block?.GetBlockName();
if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName ||
placed == RepairSkullBlockName || placed == LootSkullBlockName || placed == ProspectorSkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
if (ConnectionManager.Instance.IsServer)
{
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker || _repair || _loot || _prospector);
}
}
/// Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает.
public override void OnRemove(World _world)
{
base.OnRemove(_world);
RemoveBounds();
RemoveThreadView();
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
ZoneStop(false);
ProspectorReset();
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);
ProspectorReset();
}
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);
// v5: черепа на пути нитей - по нити: число черепов и их клетки; порядок нитей - как выше (откуда, куда, от урны).
List via = new List();
AlignVia();
foreach (List l in FromVia)
{
AppendVia(via, l);
}
AppendVia(via, ToVia);
foreach (List l in OutVia)
{
AppendVia(via, l);
}
CinerariumWriteHelper.WriteInts(_bw, via.ToArray());
// v6: ремонтник/лутер - сторона зоны, идёт ли проход, где курсор обхода, кто нажал кнопку.
CinerariumWriteHelper.WriteInts(_bw, new[] { ZoneSide, ZoneRunning ? 1 : 0, ZoneCursor, ZonePressedBy });
}
static void AppendVia(List _dst, List _l)
{
_dst.Add(_l.Count);
foreach (Vector3i p in _l)
{
_dst.Add(p.x); _dst.Add(p.y); _dst.Add(p.z);
}
}
static List TakeVia(int[] _v, ref int _k)
{
List l = new List();
int cnt = _k < _v.Length ? _v[_k++] : 0;
for (int i = 0; i < cnt && _k + 2 < _v.Length; i++, _k += 3)
{
l.Add(new Vector3i(_v[_k], _v[_k + 1], _v[_k + 2]));
}
return l;
}
static void CopyVia(List> _src, List> _dst)
{
_dst.Clear();
foreach (List l in _src)
{
_dst.Add(new List(l));
}
}
/// Списки черепов - вровень с нитями (старые записи без черепов, правки списков нитей).
void AlignVia()
{
while (FromVia.Count < ThreadFrom.Count) FromVia.Add(new List());
while (FromVia.Count > ThreadFrom.Count) FromVia.RemoveAt(FromVia.Count - 1);
while (OutVia.Count < ThreadOut.Count) OutVia.Add(new List());
while (OutVia.Count > ThreadOut.Count) OutVia.RemoveAt(OutVia.Count - 1);
if (!HasThreadTo)
{
ToVia.Clear();
}
}
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;
}
}
// Группы, которых урна ещё не знала, берут выбор группы, где их предметы лежали раньше: корм кур - «Сырое»,
// живность и транспорт (23.10) - «Прочее», стройматериалы (23.10) - «Материалы»; остальные новые - включены.
for (int g = known; g < CinerariumGroups.Count; g++)
{
int from = g == CinerariumGroups.ChickenFeed ? CinerariumGroups.Raw :
g == CinerariumGroups.Livestock || g == CinerariumGroups.Vehicles ? CinerariumGroups.Other :
g == CinerariumGroups.Building ? CinerariumGroups.Materials : -1;
bool on = from < 0 || (GroupMask & (1u << from)) != 0;
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);
}
FromVia.Clear();
OutVia.Clear();
ToVia.Clear();
if (version >= 5)
{
int n = _br.ReadInt32();
int[] v = new int[n];
for (int i = 0; i < n; i++)
{
v[i] = _br.ReadInt32();
}
int k = 0;
for (int i = 0; i < ThreadFrom.Count; i++)
{
FromVia.Add(TakeVia(v, ref k));
}
ToVia.AddRange(TakeVia(v, ref k));
for (int i = 0; i < ThreadOut.Count; i++)
{
OutVia.Add(TakeVia(v, ref k));
}
}
AlignVia();
DropRelayThreads();
ZoneSide = 0;
ZoneRunning = false;
ZoneCursor = 0;
ZonePressedBy = -1;
if (version >= 6)
{
int n = _br.ReadInt32();
int[] v = new int[n];
for (int i = 0; i < n; i++)
{
v[i] = _br.ReadInt32();
}
ZoneSide = n > 0 ? v[0] : 0;
ZoneRunning = n > 1 && v[1] != 0;
ZoneCursor = n > 2 ? v[2] : 0;
ZonePressedBy = n > 3 ? v[3] : -1;
}
_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);
// Ремонтник, лутер (CinerariumZone.cs): запуск прохода кнопкой, остановка, размер зоны.
_addCallback(new BlockActivationCommand("zurn_run", "electric_switch", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
_addCallback(new BlockActivationCommand("zurn_stop", "electric_switch", _enabled: false), TileEntityComposite.EBlockCommandOrder.Normal, FeatureData);
_addCallback(new BlockActivationCommand("zurn_size", "map_cursor", _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);
string s = _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " +
Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty");
return IsBigZone ? s + ZoneStateText() : IsProspector ? s + ProspectorStateText(_world as World) : s;
}
public override void UpdateTick(World _world)
{
base.UpdateTick(_world);
if (ZoneShown && _bounds == null)
{
SpawnBounds();
}
SyncGenie(false);
SyncThreads(_world);
if (!CinerariumLoad.Tide)
{
Work(_world);
}
}
// ------------------------------------------------------------------
// Нити (CinerariumThreads.cs).
// ------------------------------------------------------------------
bool _threadsDirty = true;
GameObject _threadView;
float _nextThreadCheck;
/// _via - черепа-ретрансляторы по порядку от урны к ящику (null - прямая нить). Нить к тому же ящику -
/// заменяет прежнюю (новый путь).
public bool AddThreadFrom(Vector3i _box, List _via = null)
{
AlignVia();
int i = ThreadFrom.IndexOf(_box);
if (i < 0)
{
if (ThreadFrom.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadFrom.Add(_box);
FromVia.Add(new List());
i = ThreadFrom.Count - 1;
}
FromVia[i] = _via != null ? new List(_via) : new List();
ThreadsChanged();
return true;
}
public void SetThreadTo(Vector3i _box, List _via = null)
{
HasThreadTo = true;
ThreadTo = _box;
ToVia.Clear();
if (_via != null)
{
ToVia.AddRange(_via);
}
ThreadsChanged();
}
public bool AddThreadOut(Vector3i _target, List _via = null)
{
AlignVia();
int i = ThreadOut.IndexOf(_target);
if (i < 0)
{
if (ThreadOut.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadOut.Add(_target);
OutVia.Add(new List());
i = ThreadOut.Count - 1;
}
OutVia[i] = _via != null ? new List(_via) : new List();
ThreadsChanged();
return true;
}
public int ClearThreads()
{
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0) + ThreadOut.Count;
ThreadFrom.Clear();
ThreadOut.Clear();
FromVia.Clear();
OutVia.Clear();
ToVia.Clear();
HasThreadTo = false;
if (n > 0)
{
ThreadsChanged();
}
return n;
}
/// Черепа-ретрансляторы отменены (23.10, пользователь: «черепа как удлинитель не работают. Думаю нам стоит
/// отказаться от их функции ретрансляторов»): нити, протянутые через черепа, при загрузке снимаются; прямые остаются.
/// Списки Via в сейве остаются (формат не меняется), но всегда пустые.
void DropRelayThreads()
{
int n = 0;
for (int i = ThreadFrom.Count - 1; i >= 0; i--)
{
if (FromVia[i].Count > 0)
{
ThreadFrom.RemoveAt(i);
FromVia.RemoveAt(i);
n++;
}
}
for (int i = ThreadOut.Count - 1; i >= 0; i--)
{
if (OutVia[i].Count > 0)
{
ThreadOut.RemoveAt(i);
OutVia.RemoveAt(i);
n++;
}
}
if (HasThreadTo && ToVia.Count > 0)
{
HasThreadTo = false;
ToVia.Clear();
n++;
}
if (n > 0)
{
_threadsDirty = true;
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + " - снято нитей через черепа: " + n);
}
}
/// Снять нити, у которых конец (_end) или череп на пути (_via) - в клетке _p.
int RemoveWhere(Vector3i _p, bool _end, bool _via)
{
AlignVia();
int n = 0;
for (int i = ThreadFrom.Count - 1; i >= 0; i--)
{
if (_end && ThreadFrom[i] == _p || _via && FromVia[i].Contains(_p))
{
ThreadFrom.RemoveAt(i);
FromVia.RemoveAt(i);
n++;
}
}
for (int i = ThreadOut.Count - 1; i >= 0; i--)
{
if (_end && ThreadOut[i] == _p || _via && OutVia[i].Contains(_p))
{
ThreadOut.RemoveAt(i);
OutVia.RemoveAt(i);
n++;
}
}
if (HasThreadTo && (_end && ThreadTo == _p || _via && ToVia.Contains(_p)))
{
HasThreadTo = false;
ToVia.Clear();
n++;
}
return n;
}
public int RemoveThreadsTo(Vector3i _box)
{
int n = RemoveWhere(_box, true, false);
if (n > 0)
{
ThreadsChanged();
}
return n;
}
/// Снять нити, идущие через череп _skull (ЛКМ ножом по черепу; череп убрали).
public int RemoveThreadsVia(Vector3i _skull)
{
int n = RemoveWhere(_skull, false, true);
if (n > 0)
{
ThreadsChanged();
}
return n;
}
/// Все черепы на нитях урны в клетках, где их больше нет (чанк загружен), - нити через них снимаются.
bool DropBrokenRelays(World _world)
{
AlignVia();
bool gone = false;
foreach (List l in AllVia())
{
foreach (Vector3i p in l)
{
if (_world.GetChunkFromWorldPos(p) != null && !CinerariumThreads.IsRelay(_world.GetBlock(p).Block))
{
if (RemoveWhere(p, false, true) > 0)
{
Debug.Log("[NecromancerTome] Cinerarium: череп-ретранслятор " + p + " убран - нить урны " + ToWorldPos() + " порвалась");
gone = true;
}
return gone; // списки изменились - остальное в следующей проверке
}
}
}
return gone;
}
IEnumerable> AllVia()
{
foreach (List l in FromVia)
{
yield return l;
}
yield return ToVia;
foreach (List l in OutVia)
{
yield return l;
}
}
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() + " сняты - нити только у логиста и рабочего");
ClearThreads();
}
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && Time.time >= _nextThreadCheck)
{
_nextThreadCheck = Time.time + 2f;
bool gone = DropBrokenRelays(_world);
foreach (Vector3i p in ThreadFrom.ToArray())
{
if (!CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u) || u != null)
{
if (_world.GetChunkFromWorldPos(p) != null) // чанк не загружен - не снимать
{
RemoveWhere(p, true, false);
gone = true;
}
}
}
if (HasThreadTo && (!CinerariumThreads.Resolve(_world, ThreadTo, out Vector3i _, out TEFeatureCinerarium u2) || u2 != null) &&
_world.GetChunkFromWorldPos(ThreadTo) != null)
{
HasThreadTo = false;
ToVia.Clear();
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)
{
RemoveWhere(p, true, false);
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();
AlignVia();
List<(Vector3 a, Vector3 b)> pairs = new List<(Vector3, Vector3)>();
for (int i = 0; i < ThreadFrom.Count; i++)
{
CinerariumThreads.AddPath(pairs, me, FromVia[i], ThreadFrom[i], true);
}
if (HasThreadTo)
{
CinerariumThreads.AddPath(pairs, me, ToVia, ThreadTo, false);
}
for (int i = 0; i < ThreadOut.Count; i++)
{
CinerariumThreads.AddPath(pairs, me, OutVia[i], ThreadOut[i], false);
}
_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;
/// Состояние работы для necrourn info (23.10: «стою в 19 м - не работают», проба находит урожай, дела нет).
public string WorkState()
{
float now = Time.time;
return "дело ждёт " + _pending + (_pending ? " (через " + (_actAt - now).ToString("0.0") + " с)" : "") +
", следующий шаг через " + (_nextWork - now).ToString("0.0") + " с, прилив " + CinerariumLoad.Tide +
", дух " + (_genie != null);
}
/// Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере.
/// Сначала пробный шаг (ничего не меняет) - куда лететь; само дело - когда дух долетел и машет руками. Если за это
/// время что-то поменялось, настоящий шаг сделает то, что нужно на тот момент.
void Work(World _world)
{
if (!HasSkull || _world == null || !ConnectionManager.Instance.IsServer)
{
_pending = false;
return;
}
if (IsBigZone)
{
ZoneWork(_world); // ремонтник, лутер - CinerariumZone.cs
return;
}
if (IsProspector)
{
ProspectorWork(_world); // старатель - CinerariumProspector.cs
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 || !CinerariumLoad.TryTakeStep(ToWorldPos()))
{
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();
// Прилив (17.10: «думаю всех некротехнологов нужно прятать во время прилива... В прилив работа тоже стоит. Урны светятся
// как с останками, но духи исчезают»): плотность некроэнергии в воздухе сравнялась с плотностью духа - вне урны он не
// держит форму. Свечение урны - по черепу, как обычно; утром дух появляется сам.
bool tide = CinerariumLoad.Tide;
if (tide && _genie != null)
{
RemoveGenie(false);
}
if (HasSkull && _genie == null && !tide)
{
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 && (IsLooter || IsProspector)) // старатель - на банке лутера
{
_genie.GetComponent().ForwardOverride = CinerariumGenie.LootForward;
}
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) || CinerariumButtons.IsButton(r))
{
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. Большая зона
// (ремонтник, лутер) - сторона ZoneSideNow, по высоте весь столб мира.
Vector3 c = World.blockToTransformPos(ToWorldPos()) - Origin.position;
int hw = IsBigZone ? ZoneSideNow / 2 : HalfWidth;
float x0 = c.x - hw - 0.5f, x1 = c.x + hw + 0.5f;
float z0 = c.z - hw - 0.5f, z1 = c.z + hw + 0.5f;
float y0 = IsBigZone ? -Origin.position.y + 1f : c.y - Below, y1 = IsBigZone ? -Origin.position.y + 254f : 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 ||
IsRepairBlock(_b) || IsLootBlock(_b) || IsProspectorBlock(_b);
}
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);
}
/// Текст подсказки, почему урну сюда нельзя (с цифрами - 18.10: «Ограничения на урны нужно сделать чёткими и
/// понятными, чтобы персонаж их мог видеть»), или null.
public static string PlacementRefusal(Vector3i _pos, Block _placing)
{
if (!IsZonelessBlock(_placing) && CinerariumRegistry.Overlaps(_pos, out Vector3i other))
{
int d = Math.Max(Math.Abs(other.x - _pos.x), Math.Abs(other.z - _pos.z));
return string.Format(Localization.Get("necroCinerariumOverlap"), d, HalfWidth * 2 + 1);
}
// 20.10 (#65, пользователь): в лимит входят ВСЕ урны - сборщик, логист, рабочий, ремонтник, лутер, старатель; пересечение
// зон проверяется только у сборщика (выше). Череп-декор урной не считается.
if (LimitNumbers(out int have, out int limit, out int kills, out int next) && have >= limit)
{
return string.Format(Localization.Get("necroCinerariumLimit"), have, limit, (int)KillsPerUrn, kills, next);
}
return null;
}
/// Урн поставлено, сколько можно, упокоено и сколько ещё упокоить до следующей урны. false - нет игрока или
/// клиент в сети (реестр живёт на сервере; мультиплеер поддержан не полностью).
public static bool LimitNumbers(out int _have, out int _limit, out int _kills, out int _next)
{
_have = _limit = _kills = _next = 0;
World world = GameManager.Instance?.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
if (player == null || world.IsRemote())
{
return false;
}
_have = CinerariumRegistry.CountFor(PlatformManager.InternalLocalUserIdentifier);
_limit = UrnLimit(player);
float kills = player.Buffs != null ? player.Buffs.GetCustomVar(Patch_EntityPlayer_AddKillXP_NecromancyCount.KillsCVarName) : 0f;
_kills = (int)kills;
_next = kills <= 0f ? Mathf.CeilToInt(KillsPerUrn) : Math.Max(1, Mathf.CeilToInt((_limit + 1) * KillsPerUrn - kills));
return true;
}
}
/// Все урны сейва: позиция и владелец, файл в папке сейва (как сумка волка). Сервер.
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)
{
// 20.10 (#65): считаются все урны владельца, любого рода (метка «L» в реестре для лимита больше не нужна).
string id = _owner != null ? _owner.CombinedString : "";
int n = 0;
foreach (KeyValuePair kv in Urns())
{
if (kv.Value == id)
{
n++;
}
}
return n;
}
public static List All()
{
return new List(Urns().Keys);
}
/// Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям,
/// по высоте ближе 6 (зона от -1 до +4).
public static bool Overlaps(Vector3i _pos) => Overlaps(_pos, out Vector3i _);
/// То же, с урной, с которой пересечётся зона.
public static bool Overlaps(Vector3i _pos, out Vector3i _other)
{
_other = _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)
{
_other = p;
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 || urn.IsBigZone);
}
else if (text.EndsWith("bounds_hide"))
{
_commands[i].enabled = urn.ZoneShown && (!urn.IsZoneless || urn.IsBigZone);
}
else if (text.EndsWith("zurn_run"))
{
_commands[i].enabled = urn.IsBigZone && urn.HasSkull && !urn.ZoneRunning;
}
else if (text.EndsWith("zurn_stop"))
{
_commands[i].enabled = urn.IsBigZone && urn.ZoneRunning;
}
else if (text.EndsWith("zurn_size"))
{
_commands[i].enabled = urn.IsBigZone;
}
}
}
}
[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");
bool zone = _commandName.EndsWith("zurn_run") || _commandName.EndsWith("zurn_stop") || _commandName.EndsWith("zurn_size");
if (!skull && !bounds && !repair && !bag && !zone)
{
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 (zone)
{
urn.ZoneCommand(_commandName, _player);
__result = true;
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);
bool repairUrn = TEFeatureCinerarium.IsRepairBlock(bv.Block);
bool lootUrn = TEFeatureCinerarium.IsLootBlock(bv.Block);
bool prospector = TEFeatureCinerarium.IsProspectorBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(prospector
? (urn.HasSkull ? TEFeatureCinerarium.ProspectorSkullBlockName : TEFeatureCinerarium.ProspectorBlockName)
: repairUrn
? (urn.HasSkull ? TEFeatureCinerarium.RepairSkullBlockName : TEFeatureCinerarium.RepairBlockName)
: lootUrn
? (urn.HasSkull ? TEFeatureCinerarium.LootSkullBlockName : TEFeatureCinerarium.LootBlockName)
: 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;
}
}
}