Files
necromants-tome-7d2d-3-2/HarmonySrc/Cinerarium.cs
T
AlexCubeandClaude Opus 5.5 0999fd3afc Цинерарий-старатель (#64): дух добывает ископаемые
Урна necroCinerariumProspector (и ...Skull): в «Содержимом» череп и образец - один из девяти ресурсов (камень, глина,
песок, древесина, железо, свинец, уголь, селитра, сланец), нить к ящику. Дух отлетает на соседнюю свободную клетку и
уходит на 5 блоков под землю, растворяясь; раз в игровой час поднимается так же, кладёт добычу в ящик и уходит снова.
За сутки: камень/глина/песок/древесина 30000, руды 18000, ходка ±20%. Из руды - шанс на алмаз, серебро, золото по
шансам игры на каждый «блок» руды. Ящик полон - дух ждёт с добычей. Без зоны и вне лимита урн.

Рецепт - кузня: латунь 40, прах 100, кости 10, кровь/сок, железная кирка; открывается с остальными урнами.
Консоль necroprospector: on/off, trip, yield, spread, depth, состояние. Тексты - 13 языков; глава книги
«Цинерарии» - лист Fig. VIII с шестой урной и фраза про prospektisto.

Проверено в игре (test8): ходка железа 625 в ящике, спуск и подъём духа, лист и страница книги помещаются.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:09:12 +03:00

1530 lines
61 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using HarmonyLib;
using Platform;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Урна-цинерарий (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 у ЛЮБОГО составного блока.
/// </summary>
public partial class TEFeatureCinerarium : TEFeatureAbs
{
public const string BlockName = "necroCinerarium";
public const string SkullBlockName = "necroCinerariumSkull";
/// <summary>Урна-логист (16.10: «Я не могу ставить урны рядом. Может нам для логиста вообще сделать отдельные урны?»):
/// без зоны - ставится вплотную к любым урнам, только нити и перевозка, глазницы всегда зелёные. Лимит - общий.</summary>
public const string LogistBlockName = "necroCinerariumLogist";
public const string LogistSkullBlockName = "necroCinerariumLogistSkull";
/// <summary>Логист у стены (18.10, кадило на кронштейне): тот же логист, другой вид - выбор в меню R (CinerariumLogistForm).</summary>
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;
}
/// <summary>Урна рабочего (17.10, BACKLOG «Урна рабочего»): изготавливает предмет из ячейки образца на станциях, к которым
/// от неё тянутся нити; ингредиенты - из ящиков с нитями к урне, готовое - в ящики с нитями от урны. Без зоны и вне лимита,
/// как логист.</summary>
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;
}
/// <summary>Урна без зоны 5x5 (логист, рабочий, ремонтник, лутер): ставится вплотную к любым урнам, в лимит не входит,
/// работает по нитям. У ремонтника и лутера своя большая зона (CinerariumZone.cs) - зоны таких урн могут пересекаться.</summary>
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;
/// <summary>Убийств на одну урну сборщика. 17.10: «лимит урн - ослабить/убрать» - нагрузку теперь держит общий бюджет
/// шагов (CinerariumLoad), лимит остался мягким игровым порогом: было 500, стало 200.</summary>
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;
/// <summary>Нити (CinerariumThreads.cs): ящики «откуда» и ящик «куда», главные клетки блоков. Есть нити - урна-логист.</summary>
public readonly List<Vector3i> ThreadFrom = new List<Vector3i>();
public bool HasThreadTo;
public Vector3i ThreadTo;
public bool HasThreads => HasThreadTo || ThreadFrom.Count > 0 || ThreadOut.Count > 0;
/// <summary>Нити рабочего «от урны»: станции (где изготавливать) и ящики (куда класть готовое), главные клетки блоков.</summary>
public readonly List<Vector3i> ThreadOut = new List<Vector3i>();
/// <summary>Черепа-ретрансляторы на пути каждой нити (18.10: «сам череп повысить из простого предмета, до промежуточного
/// ретранслятора... если понадобится протащить коммуникации дальше»; «Цепочка из нескольких»): клетки черепов-декора по
/// порядку ОТ УРНЫ к концу нити. Списки идут вровень с ThreadFrom / ThreadOut, ToVia - у нити «куда». Пусто - нить прямая.
/// Каждый пролёт - не длиннее CinerariumThreads.MaxLength. Убрали череп - нить через него снимается.</summary>
public readonly List<List<Vector3i>> FromVia = new List<List<Vector3i>>();
public readonly List<List<Vector3i>> OutVia = new List<List<Vector3i>>();
public readonly List<Vector3i> ToVia = new List<Vector3i>();
/// <summary>Образец рабочего: что изготавливать; число в стопке - сколько держать в запасе (ящики «куда» и выход станций).
/// Лежит в «Содержимом» урны второй ячейкой.</summary>
public ItemStack Sample = ItemStack.Empty.Clone();
/// <summary>Урна рабочего - по блоку, как логист.</summary>
public bool IsWorker
{
get
{
_ = IsLogist; // узнаёт тип блока
return _worker;
}
}
bool _worker;
public bool IsZoneless => IsLogist || IsWorker || IsBigZone || IsProspector;
/// <summary>Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.</summary>
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;
/// <summary>Фильтр логиста: бит на группу CinerariumGroups; по умолчанию все включены.</summary>
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<TEFeatureCinerarium>(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;
}
}
/// <summary>Блок поставлен (OnBlockAdded составного блока; на загрузке чанка не зовётся).</summary>
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);
}
}
/// <summary>Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает.</summary>
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<int> t = new List<int> { 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<int> o = new List<int> { 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<int> via = new List<int>();
AlignVia();
foreach (List<Vector3i> l in FromVia)
{
AppendVia(via, l);
}
AppendVia(via, ToVia);
foreach (List<Vector3i> 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<int> _dst, List<Vector3i> _l)
{
_dst.Add(_l.Count);
foreach (Vector3i p in _l)
{
_dst.Add(p.x); _dst.Add(p.y); _dst.Add(p.z);
}
}
static List<Vector3i> TakeVia(int[] _v, ref int _k)
{
List<Vector3i> l = new List<Vector3i>();
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<List<Vector3i>> _src, List<List<Vector3i>> _dst)
{
_dst.Clear();
foreach (List<Vector3i> l in _src)
{
_dst.Add(new List<Vector3i>(l));
}
}
/// <summary>Списки черепов - вровень с нитями (старые записи без черепов, правки списков нитей).</summary>
void AlignVia()
{
while (FromVia.Count < ThreadFrom.Count) FromVia.Add(new List<Vector3i>());
while (FromVia.Count > ThreadFrom.Count) FromVia.RemoveAt(FromVia.Count - 1);
while (OutVia.Count < ThreadOut.Count) OutVia.Add(new List<Vector3i>());
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;
}
}
// Группы, которых урна ещё не знала: корм кур раньше ехал с «Сырым» - берёт его выбор, прочие новые - включены.
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);
}
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();
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 + ") - прочитано только известное");
}
}
/// <summary>Меню как у велосипеда (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 открывает сумку.</summary>
public override void InitBlockActivationCommands(Action<BlockActivationCommand, TileEntityComposite.EBlockCommandOrder, TileEntityFeatureData> _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;
/// <summary>_via - черепа-ретрансляторы по порядку от урны к ящику (null - прямая нить). Нить к тому же ящику -
/// заменяет прежнюю (новый путь).</summary>
public bool AddThreadFrom(Vector3i _box, List<Vector3i> _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<Vector3i>());
i = ThreadFrom.Count - 1;
}
FromVia[i] = _via != null ? new List<Vector3i>(_via) : new List<Vector3i>();
ThreadsChanged();
return true;
}
public void SetThreadTo(Vector3i _box, List<Vector3i> _via = null)
{
HasThreadTo = true;
ThreadTo = _box;
ToVia.Clear();
if (_via != null)
{
ToVia.AddRange(_via);
}
ThreadsChanged();
}
public bool AddThreadOut(Vector3i _target, List<Vector3i> _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<Vector3i>());
i = ThreadOut.Count - 1;
}
OutVia[i] = _via != null ? new List<Vector3i>(_via) : new List<Vector3i>();
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;
}
/// <summary>Снять нити, у которых конец (_end) или череп на пути (_via) - в клетке _p.</summary>
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;
}
/// <summary>Снять нити, идущие через череп _skull (ЛКМ ножом по черепу; череп убрали).</summary>
public int RemoveThreadsVia(Vector3i _skull)
{
int n = RemoveWhere(_skull, false, true);
if (n > 0)
{
ThreadsChanged();
}
return n;
}
/// <summary>Все черепы на нитях урны в клетках, где их больше нет (чанк загружен), - нити через них снимаются.</summary>
bool DropBrokenRelays(World _world)
{
AlignVia();
bool gone = false;
foreach (List<Vector3i> 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<List<Vector3i>> AllVia()
{
foreach (List<Vector3i> l in FromVia)
{
yield return l;
}
yield return ToVia;
foreach (List<Vector3i> 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 : ""));
}
/// <summary>Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.</summary>
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<CinerariumThreadView>();
}
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<CinerariumThreadView>().Build(pairs);
}
void RemoveThreadView()
{
if (_threadView != null)
{
UnityEngine.Object.Destroy(_threadView);
_threadView = null;
}
}
float _nextWork;
public bool Stuck;
/// <summary>Дело найдено пробным шагом, дух полетел; выполнить в _actAt (16.10: «делал действие не сразу»).</summary>
bool _pending;
float _actAt;
/// <summary>Работа духа (CinerariumWork): раз в StepSeconds одно дело, только с черепом и только на сервере.
/// Сначала пробный шаг (ничего не меняет) - куда лететь; само дело - когда дух долетел и машет руками. Если за это
/// время что-то поменялось, настоящий шаг сделает то, что нужно на тот момент.</summary>
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<CinerariumGenieMotion>() : 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())
{
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);
}
}
}
/// <summary>Где духу встать у места работы (16.10: «влетает в станцию в самый центр... логичнее к краю станции с
/// доступной стороны»): из четырёх соседних клеток - свободная (воздух или растение), ближайшая к урне; дух встаёт у
/// её края со стороны места. Свободной нет - над местом. Возвращает низ точки в координатах мира (blockToTransformPos).</summary>
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); // чуть ближе к месту - у самого края
}
// ------------------------------------------------------------------
// Череп.
// ------------------------------------------------------------------
/// <summary>Череп положен в ячейку багажника или вынут из неё.</summary>
public void SetSkull(bool _has)
{
if (HasSkull == _has)
{
return;
}
HasSkull = _has;
SetModified();
SyncGenie(true);
Debug.Log("[NecromancerTome] Cinerarium: урна " + ToWorldPos() + (_has ? " - череп положен" : " - череп вынут"));
}
/// <summary>Образец рабочего положен, изменён или вынут.</summary>
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));
}
/// <summary>«Починить», как у транспорта: целиком, за RepairBones костей из рюкзака (сколько берёт и молоток -
/// RepairItems в blocks.xml).</summary>
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";
/// <summary>_sound - череп только что положили или вынули (порыв); при загрузке чанка дух появляется молча.</summary>
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<CinerariumGenieMotion>().ForwardOverride = CinerariumGenie.LootForward;
}
if (_genie != null && _sound)
{
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
}
}
else if (!HasSkull && _genie != null)
{
RemoveGenie(_sound);
}
}
/// <summary>Руны кровью и глазницы черепов светятся, только когда в урне череп (16.10): _EmissionColor через
/// MaterialPropertyBlock на рендерерах модели урны (материал общий не трогается), духа не задевает.</summary>
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<Renderer> own = new List<Renderer>();
foreach (Renderer r in t.GetComponentsInChildren<Renderer>(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<CinerariumBlink>();
if (blink && b == null)
{
b = t.gameObject.AddComponent<CinerariumBlink>();
}
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<LineRenderer>();
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);
}
/// <summary>Текст подсказки, почему урну сюда нельзя (с цифрами - 18.10: «Ограничения на урны нужно сделать чёткими и
/// понятными, чтобы персонаж их мог видеть»), или null.</summary>
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);
}
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;
int have = CinerariumRegistry.CountFor(me), limit = UrnLimit(player);
if (have >= limit)
{
float kills = player.Buffs != null ? player.Buffs.GetCustomVar(Patch_EntityPlayer_AddKillXP_NecromancyCount.KillsCVarName) : 0f;
int next = Mathf.CeilToInt((limit + 1) * KillsPerUrn - kills);
if (kills <= 0f)
{
next = Mathf.CeilToInt(KillsPerUrn);
}
return string.Format(Localization.Get("necroCinerariumLimit"), have, limit, (int)KillsPerUrn, (int)kills, Math.Max(1, next));
}
}
return null;
}
}
/// <summary>Все урны сейва: позиция и владелец, файл в папке сейва (как сумка волка). Сервер.</summary>
public static class CinerariumRegistry
{
const string FileName = "NecroCinerariums.txt";
static Dictionary<Vector3i, string> _urns;
/// <summary>Логисты (третье поле строки «L»): в лимит не входят.</summary>
static HashSet<Vector3i> _logists = new HashSet<Vector3i>();
static string _dir;
static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName);
static Dictionary<Vector3i, string> Urns()
{
string dir = GameIO.GetSaveGameDir();
if (_urns != null && _dir == dir)
{
return _urns;
}
_dir = dir;
_urns = new Dictionary<Vector3i, string>();
_logists = new HashSet<Vector3i>();
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<Vector3i, string> 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<Vector3i, string> 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<Vector3i> All()
{
return new List<Vector3i>(Urns().Keys);
}
/// <summary>Пересекутся ли зоны урны в _pos и уже стоящей: по горизонтали ближе 5 блоков по обеим осям,
/// по высоте ближе 6 (зона от -1 до +4).</summary>
public static bool Overlaps(Vector3i _pos) => Overlaps(_pos, out Vector3i _);
/// <summary>То же, с урной, с которой пересечётся зона.</summary>
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;
}
}
/// <summary>Какие команды меню урны доступны сейчас (текст команд общий на тип блока - см. TEFeaturePyramidWard).</summary>
[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<TEFeatureCinerarium>(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<TEFeatureStorage>(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<TEFeatureCinerarium>(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;
}
}
/// <summary>Лимит урн и пересечение зон. Составной блок CanPlaceBlockAt не переопределяет - заплатка на Block,
/// с отбором по имени блока. Зовётся и каждый кадр для призрака постановки (RenderDisplacedCube) - он
/// краснеет; причину показывает подмена подсказки «нельзя поставить» при щелчке (ниже).</summary>
[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;
}
}
}
/// <summary>Подъём урны («Take» детали TEFeaturePickup -> Block.TakeItemWithTimer): по окончании таймера игра отдаёт
/// предмет по типу блока (BlockValue.ToItemValue), OnBlockPickedUp не зовёт. Для урны - своя копия того же метода:
/// с черепом - предмет necroCinerariumSkull, без - necroCinerarium; сумка за время таймера не должна наполниться.
/// Позиция запоминается - OnRemove урны по ней понимает, что её подняли, а не разбили (череп не выбрасывать).</summary>
[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<TEFeatureCinerarium>(out TEFeatureCinerarium urn))
{
return true; // чужой блок или отказ игры (повреждён, подменён) - пусть игра сама скажет почему
}
if (composite.IsUserAccessing())
{
GameManager.ShowTooltip(player, Localization.Get("ttCantPickupInUse"), string.Empty, "ui_denied");
return false;
}
if (composite.TryGetSelfOrFeature<TEFeatureStorage>(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;
}
}
}