using System.Collections.Generic;
using System.IO;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
///
/// Ментальная форма - «Чертоги разума» (решения пользователя 02-03.10, BACKLOG «Чертоги разума»).
/// Вход - третья строка меню ножа некроманта на R, только ночью, здоровье не тратит. Тело
/// падает рэгдоллом и лежит беззащитным, как в режиме змеи; дух - камера без сущности
/// (MindControl.cs): ходит по полу, видит только своё, с другими игроками не встречается.
/// Выход - R, утро или смерть тела.
///
/// ГДЕ ЧЕРТОГИ. Под землёй, в радиационном поясе у северо-восточного угла карты: сначала от
/// угла по диагонали ищется пояс (biomeProvider.GetRadiationAt > 2.5); пояс шире префаба -
/// чертоги в его середине, уже (Navezgane: 16 блоков) - в 45 блоках от края. Своё место у каждого
/// игрока - вдоль северного края с шагом SlotStep (дальше дальности показа сущностей). Места
/// пишутся в файл в папке сохранения мира, чтобы смена ширины пояса в новой версии игры их не
/// сдвинула.
///
/// КАК СТАВЯТСЯ. Префаб chertogi (PalacePrefab/, нарисован пользователем в редакторе игры)
/// грузится Prefab.Load и ставится поблочно через World.SetBlocksRPC - НЕ Prefab.CopyIntoLocal:
/// тот перед постановкой вычищает весь столб над префабом до неба от не-земляных блоков,
/// и над чертогами вырубило бы деревья (видно сверху). Заодно: внешний слой оболочки
/// (woodShapes:cube) -> terrBedrock (в меню творчества его нет, рисовали деревом),
/// одиночная книга на столе -> necroMemoryBook, на стол с реактивами - нож-декорация,
/// кровать (SleepingBag) -> necroPalaceBed: иначе игра делает её точкой возрождения игрока.
/// Построены ли - сверка с префабом каждой клетки (Mismatches: тип, плотность, покраска) при каждом входе:
/// расхождение - ставим заново (самопочинка; заодно новый, более широкий префаб сам перестраивает старые
/// чертоги в сейвах). Надписи на досках из префаба НЕ переносятся (указание 09.10: «текст будет добавлять мод по событиям»).
///
/// ОХРАНА. Внутри чертогов игра считает место «зоной торговца» (патч IsWithinTraderArea):
/// блоки не ломаются ни ударом, ни взрывом, строить нельзя. Чужие сущности в коробке чертогов
/// раз в секунду выбрасываются: змея развеивается, игрок - на поверхность, прочие - удаляются.
///
/// ОГРАНИЧЕНИЕ: одиночная игра и хост. Клиенту в мультиплеере вход пока не даётся - чертоги ставит
/// сервер, а камера живёт у клиента (решать отдельно).
///
public static class MindPalace
{
public const string PrefabName = "chertogi";
public const string BookBlock = "necroMemoryBook";
public const string KnifeBlock = "necroKnifeDecor";
/// Кровать без класса SleepingBag - ею заменяется кровать префаба (bunkBedMadeWhite).
public const string BedBlock = "necroPalaceBed";
public const string ShellFrom = "woodShapes:cube";
public const string ShellTo = "terrBedrock";
/// Межкомнатные двери - их чертоги открывают сами при входе (кроме входной).
public const string PassageDoor = "exteriorHouseDoorWhite";
/// Одиночная книга на обеденном столе (cntBookPile09 в префабе).
public static readonly Vector3i BookAt = new Vector3i(12, 2, 9);
/// Место ножа на столе с реактивами (указание пользователя).
public static readonly Vector3i KnifeAt = new Vector3i(4, 2, 11);
/// Входная дверь чертогов - во внутреннюю оболочку, никуда не ведёт: остаётся закрытой
/// (указание 03.10). Остальные четыре - между комнатами - открываются.
public static readonly Vector3i EntranceDoorAt = new Vector3i(8, 1, 3);
/// Где появляется дух: комната с книгой, свободная клетка у стола.
public static readonly Vector3 SpawnAt = new Vector3(11.5f, 1.05f, 11.5f);
public const float SpawnYaw = 180f;
public const int SlotStep = 300;
public const int FallbackInset = 45;
public const int FloorY = 3;
/// Рамка вокруг чертогов, которая заливается камнем перед постановкой.
public const int LooseMargin = 3;
public const float RadiationThreshold = 2.5f;
/// Поворот ножа-декорации (0-23): 9 - плашмя на столешнице (подобрано пользователем
/// 03.10 командой necropalace knife N).
public static int KnifeRotation = 0;
public static bool Active;
static Vector3i _origin;
static Prefab _prefab;
static ChunkManager.ChunkObserver _observer;
enum Stage { None, Loading, Settling, Ready }
static Stage _stage;
static float _deadline;
static float _settleUntil;
static float _ragdollAt;
static bool _waitStandUp;
static float _standUpDeadline;
static float _guardCheck;
static readonly Dictionary _slots = new Dictionary();
static string _slotsFor;
public static void Init()
{
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
MindHud.Create();
}
// ---------------------------------------------------------------- места
static string SlotsFile => Path.Combine(GameIO.GetSaveGameDir(), "NecromancerTome_palaces.txt");
static void LoadSlots()
{
string dir = GameIO.GetSaveGameDir();
if (_slotsFor == dir)
{
return;
}
_slotsFor = dir;
_slots.Clear();
try
{
if (File.Exists(SlotsFile))
{
foreach (string line in File.ReadAllLines(SlotsFile))
{
string[] p = line.Split(' ');
if (p.Length == 4 && int.TryParse(p[1], out int x) && int.TryParse(p[2], out int y) && int.TryParse(p[3], out int z))
{
_slots[p[0]] = new Vector3i(x, y, z);
}
}
}
}
catch (System.Exception ex)
{
Debug.LogWarning("[NecromancerTome] MindPalace: не прочитать места - " + ex.Message);
}
}
static void SaveSlots()
{
try
{
var lines = new List();
foreach (var kv in _slots)
{
lines.Add(kv.Key + " " + kv.Value.x + " " + kv.Value.y + " " + kv.Value.z);
}
File.WriteAllLines(SlotsFile, lines);
}
catch (System.Exception ex)
{
Debug.LogWarning("[NecromancerTome] MindPalace: не записать места - " + ex.Message);
}
}
static string PlayerKey(EntityPlayer _player)
{
string id = GameManager.Instance.persistentLocalPlayer?.PrimaryId?.CombinedString;
return string.IsNullOrEmpty(id) ? "local" : id.Replace(' ', '_');
}
static Vector3i GetSlot(World _world, EntityPlayer _player)
{
LoadSlots();
string key = PlayerKey(_player);
if (_slots.TryGetValue(key, out Vector3i o))
{
return o;
}
o = ComputeSlot(_world, _slots.Count);
_slots[key] = o;
SaveSlots();
Debug.Log("[NecromancerTome] MindPalace: место " + _slots.Count + " для " + key + " - " + o);
return o;
}
/// Отступ от края: середина радиационного пояса, если он шире префаба, иначе 45.
static int Inset(World _world, Vector3i _min, Vector3i _max, int _size)
{
IBiomeProvider biomes = _world.ChunkCache?.ChunkProvider?.GetBiomeProvider();
int start = -1, end = -1;
if (biomes != null)
{
for (int t = 0; t < 600; t += 4)
{
bool rad = biomes.GetRadiationAt(_max.x - t, _max.z - t) > RadiationThreshold;
if (rad && start < 0) start = t;
if (!rad && start >= 0) { end = t; break; }
}
}
if (start >= 0 && end > start && end - start >= _size + 20)
{
return Mathf.Max(FallbackInset, (start + end) / 2 - _size / 2);
}
return FallbackInset;
}
static Vector3i ComputeSlot(World _world, int _index)
{
_world.GetWorldExtent(out Vector3i min, out Vector3i max);
Prefab p = LoadPrefab();
int size = p != null ? Mathf.Max(p.size.x, p.size.z) : 17;
int inset = Inset(_world, min, max, size);
int perEdge = Mathf.Max(1, (max.x - min.x - 2 * inset - size) / SlotStep);
int x, z;
if (_index < perEdge)
{
x = max.x - inset - size - _index * SlotStep;
z = max.z - inset - size;
}
else
{
x = max.x - inset - size;
z = max.z - inset - size - (_index - perEdge + 1) * SlotStep;
}
x = Mathf.FloorToInt(x / 16f) * 16;
z = Mathf.FloorToInt(z / 16f) * 16;
return new Vector3i(x, FloorY, z);
}
// ---------------------------------------------------------------- префаб и постройка
static string ModDir => Path.GetDirectoryName(typeof(MindPalace).Assembly.Location);
static Prefab LoadPrefab()
{
if (_prefab != null)
{
return _prefab;
}
string path = Path.Combine(ModDir, "PalacePrefab", PrefabName + ".tts");
var p = new Prefab();
if (!p.Load(new PathAbstractions.AbstractedLocation(PathAbstractions.EAbstractedLocationType.Mods, PrefabName, path, null, false)))
{
Debug.LogError("[NecromancerTome] MindPalace: префаб не загрузился - " + path);
return null;
}
_prefab = p;
Debug.Log("[NecromancerTome] MindPalace: префаб " + PrefabName + " " + p.size);
return _prefab;
}
static bool ChunksLoaded(World _world, Vector3i _o, Vector3i _size)
{
for (int cx = World.toChunkXZ(_o.x); cx <= World.toChunkXZ(_o.x + _size.x - 1); cx++)
{
for (int cz = World.toChunkXZ(_o.z); cz <= World.toChunkXZ(_o.z + _size.z - 1); cz++)
{
// Не просто «есть в памяти», а сгенерирован и украшен: иначе генерация досыпала
// землю поверх поставленного (тест 03.10: «внутри блоки камня»).
Chunk c = _world.GetChunkSync(cx, cz) as Chunk;
if (c == null || !c.IsInitialized || c.NeedsDecoration)
{
return false;
}
}
}
return true;
}
/// Стоит ли блок уже в самом префабе (03.10: пользователь расставляет книгу и нож в редакторе
/// сам) - тогда старые принудительные замены (книга в BookAt, нож в KnifeAt) не делаем.
static bool PrefabHas(Prefab _p, string _block)
{
if (_hasCache == null || _hasCacheFor != _p)
{
_hasCache = new Dictionary();
_hasCacheFor = _p;
}
if (_hasCache.TryGetValue(_block, out bool has))
{
return has;
}
int type = Block.GetBlockValue(_block).type;
for (int y = 0; y < _p.size.y && !has; y++)
for (int z = 0; z < _p.size.z && !has; z++)
for (int x = 0; x < _p.size.x && !has; x++)
{
has = _p.GetBlock(x, y, z).type == type;
}
_hasCache[_block] = has;
return has;
}
static Dictionary _hasCache;
static Prefab _hasCacheFor;
/// Что должно стоять в клетке префаба после постановки (с заменами). False - клетку
/// не трогаем (дочерняя часть многоблочного предмета).
static bool Expected(Prefab _p, int _x, int _y, int _z, out BlockValue _bv, out sbyte _d)
{
_bv = _p.GetBlock(_x, _y, _z);
_d = _p.GetDensity(_x, _y, _z);
if (_bv.ischild)
{
return false;
}
Vector3i lp = new Vector3i(_x, _y, _z);
// Пол (y 0) - НЕ bedrock: в префабе он же пол комнат, а bedrock - блок-земля, её сглаженная
// поверхность выпирала из пола в комнаты каменными наплывами (тест 03.10: «в помещении
// блоки камня»). Защита снизу - отдельный слой bedrock под префабом (FloorShield).
bool outer = _x == 0 || _z == 0 || _x == _p.size.x - 1 || _z == _p.size.z - 1 || _y == _p.size.y - 1;
if (outer && !_bv.isair && _bv.Block.GetBlockName() == ShellFrom)
{
_bv = Block.GetBlockValue(ShellTo);
_d = MarchingCubes.DensityTerrain;
}
else if (lp == BookAt && !PrefabHas(_p, BookBlock))
{
BlockValue b = Block.GetBlockValue(BookBlock);
b.rotation = _bv.rotation;
_bv = b;
_d = MarchingCubes.DensityAir;
}
else if (lp == KnifeAt && !PrefabHas(_p, KnifeBlock))
{
BlockValue k = Block.GetBlockValue(KnifeBlock);
k.rotation = (byte)KnifeRotation;
_bv = k;
_d = MarchingCubes.DensityAir;
}
else if (!_bv.isair && _bv.Block is BlockSleepingBag)
{
// Кровать префаба - просто мебель (тест 03.10: синий камень портала отправил в чертоги).
// Чертоги ставятся SetBlocksRPC без игрока, а GameManager.ChangeBlocks тогда считает
// ставившим локального игрока и любую SleepingBag-кровать делает его точкой возрождения.
BlockValue bed = Block.GetBlockValue(BedBlock);
bed.rotation = _bv.rotation;
_bv = bed;
_d = MarchingCubes.DensityAir;
}
else if (_bv.isair || !_bv.Block.shape.IsTerrain())
{
_d = MarchingCubes.DensityAir;
}
return true;
}
/// Диагностика (03.10, «нож пропал со стола»): что стоит в клетке и есть ли у блока модель.
static void LogCell(World _world, Vector3i _wp, string _what)
{
BlockValue bv = _world.GetBlock(_wp);
string s = "[NecromancerTome] MindPalace: " + _what + " " + _wp + " - " + bv.Block.GetBlockName() + " пов. " + bv.rotation;
Chunk c = _world.GetChunkFromWorldPos(_wp) as Chunk;
BlockEntityData be = c?.GetBlockEntity(_wp);
if (be == null)
{
s += ", модели нет (нет BlockEntity)";
}
else if (!be.bHasTransform || be.transform == null)
{
s += ", модель ещё не создана";
}
else
{
Renderer[] rs = be.transform.GetComponentsInChildren(true);
int on = 0;
foreach (Renderer r in rs) if (r.enabled && r.gameObject.activeInHierarchy) on++;
s += ", модель " + be.transform.name + " в " + (be.transform.position + Origin.position) + ", активна " + be.transform.gameObject.activeInHierarchy + ", рендеров " + on + "/" + rs.Length;
}
Debug.Log(s);
}
/// Сколько клеток в мире не совпадает с префабом по типу блока (0 - чертоги целы).
public static int Mismatches(World _world, Vector3i _o, List _report = null)
{
Prefab p = LoadPrefab();
if (p == null)
{
return -1;
}
int n = 0;
int shieldType = Block.GetBlockValue(ShellTo).type;
for (int z = 0; z < p.size.z; z++)
for (int x = 0; x < p.size.x; x++)
{
if (_world.GetBlock(_o + new Vector3i(x, -1, z)).type != shieldType)
{
n++;
}
}
for (int y = 0; y < p.size.y; y++)
for (int z = 0; z < p.size.z; z++)
for (int x = 0; x < p.size.x; x++)
{
Vector3i wp = _o + new Vector3i(x, y, z);
if (!Expected(p, x, y, z, out BlockValue want, out sbyte wantD))
{
// Дочерняя клетка: там не должно быть земли/камня.
BlockValue c = _world.GetBlock(wp);
if (c.Block.shape.IsTerrain() || _world.GetDensity(wp.x, wp.y, wp.z) < 0)
{
n++;
if (_report != null && _report.Count < 40)
{
_report.Add("(" + x + "," + y + "," + z + ") дочерняя клетка, стоит " + c.Block.GetBlockName() + " плотн. " + _world.GetDensity(wp.x, wp.y, wp.z));
}
}
continue;
}
BlockValue have = _world.GetBlock(wp);
// Плотность тоже: тип блока - воздух, а плотность земли - и клетка рисуется камнем
// (тест 03.10: «камень под столом, в дверном проёме, под потолком»).
sbyte haveD = _world.GetDensity(wp.x, wp.y, wp.z);
bool densityBad = (wantD < 0) != (haveD < 0);
// И покраска (указание 03.10: «покраску обязательно перенеси из префаба»).
bool paintBad = !want.isair && want.type == p.GetBlock(x, y, z).type &&
_world.GetTextureFullArray(wp.x, wp.y, wp.z) != p.GetTexture(x, y, z);
if (have.type != want.type || densityBad || paintBad)
{
n++;
if (_report != null && _report.Count < 40)
{
_report.Add("(" + x + "," + y + "," + z + ") ждали " + want.Block.GetBlockName() + " плотн. " + wantD +
", стоит " + have.Block.GetBlockName() + " плотн. " + haveD + (paintBad ? ", не та покраска" : ""));
}
}
}
return n;
}
public static bool IsBuilt(World _world, Vector3i _o)
{
return Mismatches(_world, _o) == 0;
}
/// Межкомнатные двери чертогов (PassageDoor) - открыты (в редакторе сохранились закрытыми, а дух
/// дверей не открывает), входная - закрыта. Прочие двери (шкаф-тайник, стальная - указание 09.10: «закрыты
/// и откроются только по событию») не трогаем: их открывает событие. Состояние двери - в её TEFeatureDoor.
public static void OpenDoors(World _world, Vector3i _o)
{
Vector3i s = Size;
int opened = 0;
for (int y = 0; y < s.y; y++)
for (int z = 0; z < s.z; z++)
for (int x = 0; x < s.x; x++)
{
if (_world.GetTileEntity(_o + new Vector3i(x, y, z)) is TileEntityComposite tec)
{
TEFeatureDoor door = tec.GetFeature();
if (door == null || _world.GetBlock(_o + new Vector3i(x, y, z)).Block.GetBlockName() != PassageDoor)
{
continue;
}
// Входная - закрыть (могла остаться открытой от прежних входов), остальные - открыть.
bool wantOpen = new Vector3i(x, y, z) != EntranceDoorAt;
if (door != null && door.IsOpen() != wantOpen)
{
door.SetOpen(wantOpen);
opened++;
}
}
}
if (opened > 0)
{
Debug.Log("[NecromancerTome] MindPalace: дверей переключено " + opened);
}
}
/// Надписи на досках чертогов (указание 09.10). Лор: некроманта бросили в этот мир умирать - раздели,
/// лишили артефактов (и вживлённых тоже) и ментальной магией стёрли память, «закрыв знания, опасные для общества».
/// Доска - то, что он ещё помнит или успел себе написать; строки прибавляются по событиям игры. Пока событие -
/// только день: (с какого дня, ключ локализации). Остальные строки - позднее.
public static readonly (int day, string key)[] BoardLines =
{
(1, "necroPalaceBoardRevenge"),
(5, "necroPalaceBoardRedMoon"),
};
/// При каждом входе: на каждой доске (necroChalkboard) чертогов - все наступившие строки, на языке игрока.
/// Текст из префаба в чертоги не идёт (Stamp ставит только блоки) - пишет только мод.
public static void UpdateBoards(World _world, Vector3i _o)
{
int day = GameUtils.WorldTimeToDays(_world.worldTime);
var lines = new List();
foreach (var l in BoardLines)
{
if (day >= l.day)
{
lines.Add(Localization.Get(l.key));
}
}
string text = string.Join("\n", lines);
Vector3i s = Size;
int n = 0;
for (int y = 0; y < s.y; y++)
for (int z = 0; z < s.z; z++)
for (int x = 0; x < s.x; x++)
{
Vector3i wp = _o + new Vector3i(x, y, z);
if (_world.GetBlock(wp).Block?.GetBlockName() != Chalkboard.BlockName ||
!(_world.GetTileEntity(wp) is TileEntityComposite tec) || !tec.TryGetSelfOrFeature(out TEFeatureSignable sign))
{
continue;
}
if ((sign.GetAuthoredText()?.Text ?? "") != text)
{
sign.SetText(text, true);
n++;
}
}
if (n > 0)
{
Debug.Log("[NecromancerTome] MindPalace: надпись на досках (день " + day + ", строк " + lines.Count + "), досок " + n);
}
}
public static void Stamp(World _world, Vector3i _o)
{
Prefab p = LoadPrefab();
if (p == null)
{
return;
}
// ПОРЯДОК ВАЖЕН (тест 03.10: при очистке всей коробки разом на миг открывалась пустота
// 17x8x17, земля над ней и края теряли опору - обвал, падающие блоки ушли под мир):
// 1) слой bedrock под чертогами;
// 2) оболочка - пол (y 0), стены (края по x/z) и крыша - сразу на место земли, без очистки;
// 3) очистка внутренности (её уже держит оболочка) - в том числе дочерних клеток стола,
// дверей, шкафов, где иначе оставался камень;
// 4) всё внутреннее по префабу.
var list = new List();
// 0) Рамка LooseMargin блоков вокруг чертогов, со всех сторон и сверху, - сплошь камнем
// (указание 03.10: «заполни сначала камнем, на 3 блока во все стороны»): земля, песок,
// гравий (осыпаются без опоры) и пустоты пещер -> terrStone. Не земля и не воздух
// (чужие постройки) - не трогаем.
BlockValue stone = Block.GetBlockValue("terrStone");
int loose = 0;
for (int y = -1 - LooseMargin; y < p.size.y + LooseMargin; y++)
for (int z = -LooseMargin; z < p.size.z + LooseMargin; z++)
for (int x = -LooseMargin; x < p.size.x + LooseMargin; x++)
{
bool inside = y >= -1 && y < p.size.y && x >= 0 && x < p.size.x && z >= 0 && z < p.size.z;
if (inside)
{
continue;
}
Vector3i wp = _o + new Vector3i(x, y, z);
if (wp.y < 1)
{
continue; // ниже 1 игра блоки не ставит (дно мира)
}
BlockValue have = _world.GetBlock(wp);
Block b = have.Block;
if (have.type != stone.type && (have.isair || b != null && b.shape.IsTerrain()))
{
list.Add(new BlockChangeInfo(wp, stone, MarchingCubes.DensityTerrain));
loose++;
}
}
if (loose > 0)
{
Debug.Log("[NecromancerTome] MindPalace: рамка вокруг залита камнем - " + loose + " клеток");
}
BlockValue shield = Block.GetBlockValue(ShellTo);
for (int z = 0; z < p.size.z; z++)
for (int x = 0; x < p.size.x; x++)
{
list.Add(new BlockChangeInfo(_o + new Vector3i(x, -1, z), shield, MarchingCubes.DensityTerrain));
}
for (int pass = 0; pass < 3; pass++)
for (int yi = 0; yi < p.size.y; yi++)
for (int z = 0; z < p.size.z; z++)
for (int x = 0; x < p.size.x; x++)
{
// Очистка (проход 1) - СВЕРХУ ВНИЗ: снизу вверх убранный слой оставлял над собой
// песок/гравий без опоры, тот падал и рушил уже поставленные стены (тест 03.10,
// necropalace test: 30 расхождений сразу, 309 - при следующем входе). Постановка -
// снизу вверх, на опору.
int y = pass == 1 ? p.size.y - 1 - yi : yi;
bool shellCell = y == 0 || y == p.size.y - 1 || x == 0 || z == 0 || x == p.size.x - 1 || z == p.size.z - 1;
Vector3i wp = _o + new Vector3i(x, y, z);
if (pass == 0 && shellCell || pass == 2 && !shellCell)
{
if (Expected(p, x, y, z, out BlockValue bv, out sbyte d))
{
// С покраской из префаба (указание 03.10).
list.Add(new BlockChangeInfo(wp, bv, d, p.GetTexture(x, y, z)));
}
}
else if (pass == 1 && !shellCell)
{
list.Add(new BlockChangeInfo(wp, BlockValue.Air, MarchingCubes.DensityAir));
}
}
_world.SetBlocksRPC(list);
Debug.Log("[NecromancerTome] MindPalace: чертоги поставлены в " + _o + ", блоков " + list.Count);
}
static Vector3i Size => _prefab != null ? _prefab.size : new Vector3i(17, 8, 17);
/// Проверка постановки в нетронутую землю (указание 03.10): залить камнем коробку на
/// 2 блока шире чертогов во все стороны (включая слой bedrock снизу), подождать, поставить
/// чертоги заново, выставить двери. Сразу заливкой, без предварительной очистки - очистка и
/// есть та пустота без опоры, из-за которой был обвал.
public static System.Collections.IEnumerator StressTest(World _world, Vector3i _o)
{
Vector3i s = Size;
// Гравием - худший случай: сыпучее, падает без опоры (указание 03.10).
BlockValue stone = Block.GetBlockValue("terrGravel");
var list = new List();
for (int y = -3; y < s.y + 2; y++)
for (int z = -2; z < s.z + 2; z++)
for (int x = -2; x < s.x + 2; x++)
{
list.Add(new BlockChangeInfo(_o + new Vector3i(x, y, z), stone, MarchingCubes.DensityTerrain));
}
_world.SetBlocksRPC(list);
Debug.Log("[NecromancerTome] necropalace test: залито гравием " + list.Count + " клеток, ждём 2 с");
yield return new WaitForSeconds(2f);
Stamp(_world, _o);
yield return new WaitForSeconds(1f);
OpenDoors(_world, _o);
Debug.Log("[NecromancerTome] necropalace test: готово, расхождений с префабом " + Mismatches(_world, _o));
}
public static bool InAnyPalace(Vector3i _pos, int _margin = 0)
{
if (_slots.Count == 0)
{
return false;
}
Vector3i s = Size;
foreach (Vector3i o in _slots.Values)
{
if (_pos.x >= o.x - _margin && _pos.x < o.x + s.x + _margin &&
_pos.y >= o.y - 1 - _margin && _pos.y < o.y + s.y + _margin &&
_pos.z >= o.z - _margin && _pos.z < o.z + s.z + _margin)
{
return true;
}
}
return false;
}
// ---------------------------------------------------------------- вход и выход
public static void Enter(EntityPlayerLocal _player)
{
if (Active || SnakeMode.Active || _player == null || _player.IsDead() || _player.AttachedToEntity != null)
{
return;
}
if (!SnakeMode.IsNight())
{
Debug.Log("[NecromancerTome] MindPalace: днём чертоги недоступны");
return;
}
World world = GameManager.Instance.World;
if (world.IsRemote())
{
Debug.Log("[NecromancerTome] MindPalace: в мультиплеере у клиента пока недоступно");
return;
}
if (LoadPrefab() == null)
{
return;
}
_origin = GetSlot(world, _player);
Vector3 center = _origin.ToVector3() + new Vector3(Size.x * 0.5f, 0f, Size.z * 0.5f);
// Дальность как у змеи: при радиусе 2 игра не строила коллизии участка (тест 03.10:
// «чертоги не построились вовремя» - ждали коллизий 30 с).
_observer = GameManager.Instance.AddChunkObserver(center, true, GameUtils.GetViewDistance(), -1);
_stage = Stage.Loading;
_deadline = Time.time + 30f;
Active = true;
_player.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 1f, 0.6f);
_player.SetFirstPersonView(false, true);
_ragdollAt = Time.time + SnakeMode.RagdollDelay;
_waitStandUp = false;
Debug.Log("[NecromancerTome] MindPalace: вход, чертоги в " + _origin);
}
public static void Exit(EntityPlayerLocal _player)
{
if (!Active)
{
return;
}
Active = false;
_stage = Stage.None;
MindControl.End(_player);
RemoveObserver();
_player?.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 0f, 1f);
if (_player == null || _player.IsDead())
{
return;
}
_ragdollAt = 0f;
_player.emodel.ragdollTime = _player.emodel.ragdollDuration;
_waitStandUp = true;
_standUpDeadline = Time.time + 5f;
Debug.Log("[NecromancerTome] MindPalace: выход");
}
static void RemoveObserver()
{
if (_observer != null && GameManager.Instance?.World != null)
{
GameManager.Instance.RemoveChunkObserver(_observer);
}
_observer = null;
}
static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
{
World world = GameManager.Instance?.World;
if (world == null)
{
Active = false;
_stage = Stage.None;
_observer = null;
MindControl.End(null);
_slotsFor = null;
return;
}
EntityPlayerLocal player = world.GetPrimaryPlayer();
Guard(world);
if (!Active)
{
if (_waitStandUp && player != null && (!player.emodel.IsRagdollActive || Time.time > _standUpDeadline))
{
_waitStandUp = false;
player.SetFirstPersonView(true, true);
}
return;
}
if (player == null)
{
return;
}
if (player.IsDead())
{
Debug.Log("[NecromancerTome] MindPalace: тело убито");
Exit(player);
return;
}
if (!SnakeMode.IsNight())
{
Debug.Log("[NecromancerTome] MindPalace: наступило утро - обратно в тело");
Exit(player);
return;
}
Body(player);
switch (_stage)
{
case Stage.Loading:
if (ChunksLoaded(world, _origin, Size))
{
var report = new List();
int bad = Mismatches(world, _origin, report);
if (bad != 0)
{
Debug.Log("[NecromancerTome] MindPalace: расхождений с префабом " + bad + " - ставим заново");
for (int i = 0; i < report.Count && i < 10; i++)
{
Debug.Log("[NecromancerTome] MindPalace: " + report[i]);
}
Stamp(world, _origin);
}
_stage = Stage.Settling;
_settleUntil = Time.time + 1.5f;
}
else if (Time.time > _deadline)
{
Debug.LogWarning("[NecromancerTome] MindPalace: участки чертогов не загрузились");
Exit(player);
}
break;
case Stage.Settling:
Vector3 spawn = _origin.ToVector3() + SpawnAt;
// Коллизии готовы - или прошло 5 с: тогда всё равно пускаем, от провала страхует
// MindControl (возврат на точку появления).
if (Time.time > _settleUntil && (world.IsChunkAreaCollidersLoaded(spawn) || Time.time > _settleUntil + 5f))
{
Debug.Log("[NecromancerTome] MindPalace: коллизии " + (world.IsChunkAreaCollidersLoaded(spawn) ? "готовы" : "НЕ готовы, пускаем по таймеру"));
_stage = Stage.Ready;
OpenDoors(world, _origin);
UpdateBoards(world, _origin);
LogCell(world, _origin + KnifeAt, "нож");
LogCell(world, _origin + BookAt, "книга");
MindControl.Begin(player, spawn, SpawnYaw);
player.ScreenEffectManager?.SetScreenEffect("FadeToBlack", 0f, 1.5f);
}
else if (Time.time > _deadline)
{
Debug.LogWarning("[NecromancerTome] MindPalace: чертоги не построились вовремя");
Exit(player);
}
break;
case Stage.Ready:
MindControl.Update(player);
break;
}
// Выход по R - когда тело уже лежит и не открыто окно (как у змеи).
PlayerActionsLocal input = player.playerInput;
bool gui = player.PlayerUI != null && player.PlayerUI.windowManager.IsModalWindowOpen();
if (_ragdollAt <= 0f && player.emodel.IsRagdollActive && input != null && !gui &&
(input.Reload.WasPressed || input.PermanentActions.Reload.WasPressed))
{
Exit(player);
}
}
/// Тело падает и лежит - как в режиме змеи (SnakeMode.OnUnityUpdate).
static void Body(EntityPlayerLocal _player)
{
if (_ragdollAt > 0f)
{
if (Time.time >= _ragdollAt)
{
_ragdollAt = 0f;
_player.emodel.DoRagdoll(EModelBase.RagdollMode.Default, 999999f, EnumBodyPartHit.None, Vector3.zero, Vector3.zero, false);
}
}
else if (!_player.emodel.IsRagdollActive)
{
_player.emodel.DoRagdoll(EModelBase.RagdollMode.Default, 999999f, EnumBodyPartHit.None, Vector3.zero, Vector3.zero, false);
}
}
/// Раз в секунду: всё живое в коробке чертогов - вон. Дух сущности не имеет.
static void Guard(World _world)
{
_guardCheck -= Time.deltaTime;
if (_guardCheck > 0f || _world.IsRemote())
{
return;
}
_guardCheck = 1f;
LoadSlots();
if (_slots.Count == 0)
{
return;
}
// Игроков в EntityAlives нет (World.SpawnEntityInWorld кладёт туда только не-игроков) - их
// отдельно, по Players.list (тест 03.10: камень портала привёл в чертоги, охрана молчала).
for (int i = _world.Players.list.Count - 1; i >= 0; i--)
{
EntityPlayer pl = _world.Players.list[i];
if (pl == null || pl.IsDead() || !InAnyPalace(World.worldToBlockPos(pl.position)))
{
continue;
}
Vector3i b = World.worldToBlockPos(pl.position);
Vector3 up = new Vector3(pl.position.x, _world.GetTerrainHeight(b.x, b.z) + 2f, pl.position.z);
Debug.Log("[NecromancerTome] MindPalace: игрок " + pl.entityId + " в чертогах - выброшен на " + up);
if (pl is EntityPlayerLocal)
{
// Как камень портала (PortalStonePatch.TeleportToBedroll).
NetPackageManager.GetPackage().Setup(up, null).ProcessPackage(_world, GameManager.Instance);
}
else
{
pl.Teleport(up);
}
}
for (int i = _world.EntityAlives.Count - 1; i >= 0; i--)
{
EntityAlive e = _world.EntityAlives[i];
if (e == null || e.IsDead() || !InAnyPalace(World.worldToBlockPos(e.position)))
{
continue;
}
EntityAlive snake = SnakeMode.Snake(_world);
if (snake != null && e == snake)
{
Debug.Log("[NecromancerTome] MindPalace: змея в чертогах - развеяна");
SnakeMode.Exit(_world.GetPrimaryPlayer());
}
else
{
Debug.Log("[NecromancerTome] MindPalace: " + e.EntityName + " в чертогах - удалён");
_world.RemoveEntity(e.entityId, EnumRemoveEntityReason.Despawned);
}
}
}
}
/// В чертогах и в рамке камня вокруг ничего не падает (указание 03.10: «почему не
/// отключить просчёт физики в той области»): World.AddFallingBlock - единственная очередь
/// падающих блоков (туда же игра сама не пускает блоки со StabilityIgnore). Тест гравием
/// 03.10: после постановки нижний/верхний ряд внутренней стены всё равно обрушивался.
[HarmonyPatch(typeof(World), nameof(World.AddFallingBlock))]
public static class Patch_World_AddFallingBlock_MindPalace
{
public static bool Prefix(Vector3i _blockPos)
{
return !MindPalace.InAnyPalace(_blockPos, MindPalace.LooseMargin);
}
}
/// Чертоги - «зона торговца»: блоки не ломаются ударом и взрывом, строить нельзя
/// (ItemActionAttack, Explosion, Block.DamageBlock проверяют IsWithinTraderArea).
[HarmonyPatch(typeof(World), nameof(World.IsWithinTraderArea), new[] { typeof(Vector3i) })]
public static class Patch_World_IsWithinTraderArea_MindPalace
{
public static void Postfix(Vector3i _worldBlockPos, ref bool __result)
{
if (!__result && MindPalace.InAnyPalace(_worldBlockPos))
{
__result = true;
}
}
}
///
/// necropalace - отладка чертогов: where (где мои), rebuild (поставить заново, если участки
/// загружены), knife N (поворот ножа 0-23 и переставить его).
///
[Preserve]
public class ConsoleCmdNecroPalace : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necropalace" };
public override string getDescription() => "NecromancerTome: чертоги разума - where | scan | rebuild | test | knife N";
public override void Execute(List _params, CommandSenderInfo _senderInfo)
{
World world = GameManager.Instance.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
if (player == null)
{
return;
}
Vector3i o = Traverse.Create(typeof(MindPalace)).Method("GetSlot", world, (EntityPlayer)player).GetValue();
string cmd = _params.Count > 0 ? _params[0] : "where";
if (cmd == "knife" && _params.Count > 1 && int.TryParse(_params[1], out int r))
{
MindPalace.KnifeRotation = Mathf.Clamp(r, 0, 3);
BlockValue k = Block.GetBlockValue(MindPalace.KnifeBlock);
k.rotation = (byte)MindPalace.KnifeRotation;
world.SetBlockRPC(o + MindPalace.KnifeAt, k, MarchingCubes.DensityAir);
SdtdConsole.Instance.Output("нож: поворот " + MindPalace.KnifeRotation);
}
else if (cmd == "scan")
{
var report = new List();
int n = MindPalace.Mismatches(world, o, report);
SdtdConsole.Instance.Output("расхождений с префабом: " + n);
foreach (string line in report)
{
SdtdConsole.Instance.Output(line);
Debug.Log("[NecromancerTome] necropalace scan: " + line);
}
}
else if (cmd == "test")
{
GameManager.Instance.StartCoroutine(MindPalace.StressTest(world, o));
SdtdConsole.Instance.Output("проверка: заливка гравием, через 2 с - постановка (итог - в логе)");
}
else if (cmd == "rebuild")
{
MindPalace.Stamp(world, o);
SdtdConsole.Instance.Output("чертоги поставлены заново в " + o);
}
else
{
SdtdConsole.Instance.Output("чертоги: " + o + ", построены: " + MindPalace.IsBuilt(world, o));
}
}
}
}