Files
necromants-tome-7d2d-3-2/HarmonySrc/Cinerarium.cs
T

1125 lines
43 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 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";
public static bool IsLogistBlock(Block _b)
{
string n = _b?.GetBlockName();
return n == LogistBlockName || n == LogistSkullBlockName;
}
public const string SkullItemName = "necroSkull";
public const int HalfWidth = 2;
public const int Below = 1;
public const int Above = 4;
public const float KillsPerUrn = 500f;
const byte SaveVersion = 3; // 2 - нити, 3 - фильтр логиста (16.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;
/// <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);
_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;
GroupMask = other.GroupMask;
_threadsDirty = true;
}
}
/// <summary>Блок поставлен (OnBlockAdded составного блока; на загрузке чанка не зовётся).</summary>
public override void OnAdded(Vector3i _blockPos, BlockValue _blockValue)
{
base.OnAdded(_blockPos, _blockValue);
_logist = IsLogistBlock(_blockValue.Block);
_logistKnown = true;
string placed = _blockValue.Block?.GetBlockName();
if (placed == SkullBlockName || placed == LogistSkullBlockName)
{
HasSkull = true; // поставили урну, поднятую с черепом
}
if (ConnectionManager.Instance.IsServer)
{
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist);
}
}
/// <summary>Блок убран (подобран или разрушен; выгрузка чанка - OnUnload). Череп не пропадает.</summary>
public override void OnRemove(World _world)
{
base.OnRemove(_world);
RemoveBounds();
RemoveThreadView();
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
if (!ConnectionManager.Instance.IsServer)
{
return;
}
Vector3i pos = ToWorldPos();
CinerariumRegistry.Remove(pos);
// Подняли целиком - череп уехал вместе с урной (necroCinerariumSkull), не выбрасывать.
bool pickedUp = Patch_Block_TakeItemWithTimerDone_Cinerarium.WasPickedUp(pos);
if (HasSkull && _world != null && !pickedUp)
{
HasSkull = false;
ItemValue skull = ItemClass.GetItem(SkullItemName);
if (skull != null && skull.type > 0)
{
_world.gameManager.ItemDropServer(new ItemStack(skull, 1), World.blockToTransformPos(pos) + Vector3.up * 0.7f, Vector3.zero);
Debug.Log("[NecromancerTome] Cinerarium: урна " + pos + " убрана - череп выпал");
}
}
}
public override void OnUnload(World _world)
{
base.OnUnload(_world);
RemoveBounds();
RemoveThreadView();
_threadsDirty = true; // чанк вернётся - нити построить заново
RemoveGenie(false);
CinerariumContents.CloseIfShowing(this);
}
public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode)
{
base.Write(_bw, _eStreamMode);
CinerariumWriteHelper.Write(_bw, SaveVersion, HasSkull, ZoneShown);
List<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) });
}
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();
for (int i = 0; i < n; i++)
{
int v = _br.ReadInt32();
if (i == 0)
{
GroupMask = unchecked((uint)v);
}
}
}
_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);
}
public override string GetActivationText(WorldBase _world, Vector3i _blockPos, BlockValue _blockValue, EntityAlive _entityFocusing, string _activateHotkeyMarkup, string _focusedTileEntityName)
{
base.GetActivationText(_world, _blockPos, _blockValue, _entityFocusing, _activateHotkeyMarkup, _focusedTileEntityName);
return _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " +
Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty");
}
public override void UpdateTick(World _world)
{
base.UpdateTick(_world);
if (ZoneShown && _bounds == null)
{
SpawnBounds();
}
SyncGenie(false);
SyncThreads(_world);
Work(_world);
}
// ------------------------------------------------------------------
// Нити (CinerariumThreads.cs).
// ------------------------------------------------------------------
bool _threadsDirty = true;
GameObject _threadView;
float _nextThreadCheck;
public bool AddThreadFrom(Vector3i _box)
{
if (ThreadFrom.Contains(_box))
{
return true;
}
if (ThreadFrom.Count >= CinerariumThreads.MaxFrom)
{
return false;
}
ThreadFrom.Add(_box);
ThreadsChanged();
return true;
}
public void SetThreadTo(Vector3i _box)
{
HasThreadTo = true;
ThreadTo = _box;
ThreadsChanged();
}
public int ClearThreads()
{
int n = ThreadFrom.Count + (HasThreadTo ? 1 : 0);
ThreadFrom.Clear();
HasThreadTo = false;
if (n > 0)
{
ThreadsChanged();
}
return n;
}
public int RemoveThreadsTo(Vector3i _box)
{
int n = ThreadFrom.RemoveAll(p => p == _box);
if (HasThreadTo && ThreadTo == _box)
{
HasThreadTo = false;
n++;
}
if (n > 0)
{
ThreadsChanged();
}
return n;
}
void ThreadsChanged()
{
_threadsDirty = true;
_glowState = -1; // логист - другие глаза
SetModified();
Debug.Log("[NecromancerTome] Cinerarium: нити урны " + ToWorldPos() + " - откуда " + ThreadFrom.Count + ", куда " + (HasThreadTo ? ThreadTo.ToString() : "нет"));
}
/// <summary>Нити к убранным ящикам снимаются (раз в 2 с); вид перестраивается после изменений.</summary>
void SyncThreads(World _world)
{
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && !IsLogist && _logistKnown)
{
Debug.Log("[NecromancerTome] Cinerarium: нити у обычной урны " + ToWorldPos() + " сняты - нити только у урны-логиста");
ThreadFrom.Clear();
HasThreadTo = false;
ThreadsChanged();
}
if (_world != null && ConnectionManager.Instance.IsServer && HasThreads && Time.time >= _nextThreadCheck)
{
_nextThreadCheck = Time.time + 2f;
bool gone = false;
foreach (Vector3i p in ThreadFrom.ToArray())
{
if (!CinerariumThreads.Resolve(_world, p, out Vector3i _, out TEFeatureCinerarium u) || u != null)
{
if (_world.GetChunkFromWorldPos(p) != null) // чанк не загружен - не снимать
{
ThreadFrom.Remove(p);
gone = true;
}
}
}
if (HasThreadTo && (!CinerariumThreads.Resolve(_world, ThreadTo, out Vector3i _, out TEFeatureCinerarium u2) || u2 != null) &&
_world.GetChunkFromWorldPos(ThreadTo) != null)
{
HasThreadTo = false;
gone = true;
}
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();
List<(Vector3 a, Vector3 b)> pairs = new List<(Vector3, Vector3)>();
foreach (Vector3i p in ThreadFrom)
{
pairs.Add((CinerariumThreads.BoxPoint(p, me), CinerariumThreads.UrnPoint(me)));
}
if (HasThreadTo)
{
pairs.Add((CinerariumThreads.UrnPoint(me), CinerariumThreads.BoxPoint(ThreadTo, me)));
}
_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;
}
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)
{
return;
}
_nextWork = Time.time + CinerariumWork.StepSeconds;
Vector3i? at = CinerariumWork.Step(_world, this, out bool stuck, true);
if (stuck != Stuck)
{
Stuck = stuck;
Debug.Log("[NecromancerTome] Cinerarium: дух урны " + ToWorldPos() + (stuck ? " почернел - готовое класть некуда" : " ожил"));
}
if (motion != null)
{
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>«Починить», как у транспорта: целиком, за 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();
if (HasSkull && _genie == null)
{
_genie = CinerariumGenie.Spawn(ModelTransform());
if (_genie != null && _sound)
{
Audio.Manager.BroadcastPlay(World.blockToTransformPos(ToWorldPos()), GenieSound);
}
}
else if (!HasSkull && _genie != null)
{
RemoveGenie(_sound);
}
}
/// <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();
// Логист (16.10: «Так сразу будет видно что логистика, а что технолог») - всегда своя текстура свечения (руны и
// глазницы зелёные) и зеленоватая глазурь; светится, как и обычная урна, только с черепом.
int want = (HasSkull ? 1 : 0) + (IsLogist ? 2 : 0);
Texture logist = IsLogist ? LogistEmission() : null;
if (t == null || (_glowState == want && _glowModel == t))
{
return;
}
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))
{
continue;
}
r.GetPropertyBlock(_glowBlock);
_glowBlock.SetColor(EmissionColorId, HasSkull ? GlowColor : Color.black);
if (logist != null)
{
_glowBlock.SetTexture(EmissionMapId, logist);
_glowBlock.SetColor(AlbedoColorId, LogistTint);
}
r.SetPropertyBlock(_glowBlock);
}
_glowState = want;
_glowModel = t;
}
/// <summary>Глазурь урны-логиста - множитель цвета материала (чёрная глазурь с зелёным отливом).</summary>
public static Color LogistTint = new Color(0.55f, 1f, 0.75f, 1f);
static readonly int AlbedoColorId = Shader.PropertyToID("_Color");
static readonly int EmissionMapId = Shader.PropertyToID("_EmissionMap");
static Texture2D _logistTex;
static bool _logistTried;
public const string LogistTextureFile = "necroCinerarium_e_logist.png";
/// <summary>Свечение урны-логиста (16.10: «пусть глаза черепов светятся другим цветом. Зелёным»): копия запечённой
/// necroCinerarium_e с зелёными глазницами (руны - те же), из папки Resources мода - бандл не пересобирается.</summary>
static Texture2D LogistEmission()
{
if (!_logistTried)
{
_logistTried = true;
try
{
string path = Path.Combine(ModEntry.Instance.Path, "Resources", LogistTextureFile);
Texture2D tex = new Texture2D(2, 2, TextureFormat.RGBA32, true);
if (tex.LoadImage(File.ReadAllBytes(path)))
{
tex.name = "necroCinerarium_e_logist";
tex.anisoLevel = 4;
_logistTex = tex;
}
}
catch (Exception e)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: свечение логиста не загружено (" + e.Message + ")");
}
}
return _logistTex;
}
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.
Vector3 c = World.blockToTransformPos(ToWorldPos()) - Origin.position;
float x0 = c.x - HalfWidth - 0.5f, x1 = c.x + HalfWidth + 0.5f;
float z0 = c.z - HalfWidth - 0.5f, z1 = c.z + HalfWidth + 0.5f;
float y0 = c.y - Below, y1 = c.y + Above + 1f;
Vector3 b0 = new Vector3(x0, y0, z0), b1 = new Vector3(x1, y0, z0), b2 = new Vector3(x1, y0, z1), b3 = new Vector3(x0, y0, z1);
Vector3 t0 = new Vector3(x0, y1, z0), t1 = new Vector3(x1, y1, z0), t2 = new Vector3(x1, y1, z1), t3 = new Vector3(x0, y1, z1);
// Одна ломаная по всем 12 рёбрам (часть рёбер проходится дважды - на глаз не видно).
Vector3[] path = { b0, b1, b2, b3, b0, t0, t1, b1, t1, t2, b2, t2, t3, b3, t3, t0 };
lr.positionCount = path.Length;
for (int i = 0; i < path.Length; i++)
{
lr.SetPosition(i, t.InverseTransformPoint(path[i]));
}
}
public void RemoveBounds()
{
if (_bounds != null)
{
UnityEngine.Object.Destroy(_bounds);
_bounds = null;
}
}
// ------------------------------------------------------------------
// Постановка: лимит и пересечение зон.
// ------------------------------------------------------------------
public static bool IsUrn(Block _b)
{
string n = _b?.GetBlockName();
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName;
}
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>Ключ подсказки, почему урну сюда нельзя, или null.</summary>
public static string PlacementRefusal(Vector3i _pos, Block _placing)
{
if (!IsLogistBlock(_placing) && CinerariumRegistry.Overlaps(_pos))
{
return "necroCinerariumOverlap";
}
if (IsLogistBlock(_placing))
{
return null; // логисты в лимит не входят (16.10, пользователь)
}
World world = GameManager.Instance?.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
// Реестр живёт на сервере; клиент в сети лимит не проверяет (мультиплеер поддержан не полностью).
if (player != null && world != null && !world.IsRemote())
{
PlatformUserIdentifierAbs me = PlatformManager.InternalLocalUserIdentifier;
if (CinerariumRegistry.CountFor(me) >= UrnLimit(player))
{
return "necroCinerariumLimit";
}
}
return null;
}
}
/// <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.IsLogistBlock(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)
{
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.IsLogistBlock(world.GetBlock(p).Block))
{
continue; // у логиста зоны нет
}
if (p != _pos && Math.Abs(p.x - _pos.x) <= w && Math.Abs(p.z - _pos.z) <= w && Math.Abs(p.y - _pos.y) <= h)
{
return true;
}
}
return false;
}
}
/// <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.IsLogist;
}
else if (text.EndsWith("bounds_hide"))
{
_commands[i].enabled = urn.ZoneShown && !urn.IsLogist;
}
}
}
}
[HarmonyPatch(typeof(BlockCompositeTileEntity), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })]
public static class Patch_BlockCompositeTileEntity_OnBlockActivated_Cinerarium
{
public static bool Prefix(string _commandName, Vector3i _blockPos, EntityPlayerLocal _player, ref bool __result)
{
if (string.IsNullOrEmpty(_commandName))
{
return true;
}
bool skull = _commandName.EndsWith(":trunk") || _commandName == "trunk";
bool repair = _commandName.EndsWith("urn_repair");
bool bag = _commandName.EndsWith("TEFeatureCinerarium:bag");
bool bounds = _commandName.EndsWith("bounds_show") || _commandName.EndsWith("bounds_hide");
if (!skull && !bounds && !repair && !bag)
{
return true;
}
World world = GameManager.Instance?.World;
if (!(world?.GetTileEntity(_blockPos) is TileEntityComposite composite) ||
!composite.TryGetSelfOrFeature<TEFeatureCinerarium>(out TEFeatureCinerarium urn))
{
return true;
}
if (bag)
{
__result = composite.OnBlockActivated(null, "TEFeatureStorage:Search", world, _blockPos, world.GetBlock(_blockPos), _player);
return false;
}
if (skull)
{
CinerariumContents.Open(_player, urn);
}
else if (repair)
{
if (!TEFeatureCinerarium.Repair(world, _blockPos, _player))
{
GameManager.ShowTooltip(_player, "necroCinerariumNoBones");
}
}
else
{
urn.ZoneShown = !urn.ZoneShown;
if (urn.ZoneShown)
{
urn.SpawnBounds();
}
else
{
urn.RemoveBounds();
}
urn.SetModified();
}
__result = true;
return false;
}
}
/// <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())
{
GameManager.ShowTooltip(player, Localization.Get("necroCinerariumBagNotEmpty"), string.Empty, "ui_denied");
return false;
}
bool logist = TEFeatureCinerarium.IsLogistBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(logist
? (urn.HasSkull ? TEFeatureCinerarium.LogistSkullBlockName : TEFeatureCinerarium.LogistBlockName)
: (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;
}
}
}