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)); } } } }