using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Обыск одержимой крысой (указание 08.10). Крыса - разведчик: «она может осмотреть ящики, и если там /// что-то интересное, персонаж придёт и сам залутает». Поэтому - только смотреть. /// /// Окно ванильное, ровно как у персонажа (таймер первого обыска, сетка, звуки): контейнер открывается /// той же цепочкой LockManager.LockRequestLocal -> OnLockResponseLocal -> XUiC_LootWindowGroup.OpenLooting. /// Отличия на время такого просмотра (Viewing): /// - рюкзак персонажа не показывается («сумку персонажа вообще не показывать») - openContainer /// открывает его сам, закрываем сразу же; /// - ячейки контейнера заперты (XUiC_ItemStack.IsLocked), «Взять всё» (R) и сортировка не работают; /// попытка взять - отказ, как при переполненном рюкзаке (указание: «делал бы вид, что сумки /// переполнены»): звук ui_denied и xuiInventoryFullForPickup; /// - окно не закрывается из-за дальности до ТЕЛА: XUiC_LootWindow.Update меряет её от персонажа. /// Подсказку «E: обыскать» рисует управление игрока, а оно в одержимости выключено - выводим сами. /// /// МЕШКИ (указание 09.10: «лутать жёлтые мешочки, и синие, и красные, падающие с зомби»). Мешок - сущность /// EntityLootContainer, открывается той же цепочкой замка (LockRequestLocal -> Entity.OnLockResponseLocal), но /// окно другое - XUiC_BagStorageWindowGroup; для него те же отличия: рюкзак закрыт, ячейки заперты, дальность /// от крысы. Рюкзак погибшего игрока (EntityBackpack) - не мешок, его не трогаем. /// public static class RatLoot { /// Открыто (или открывается) окно просмотра крысой. public static bool Viewing; /// Окно открыл волк: щелчок и «Взять всё» кладут в его сумку (WolfBag), а не отказ. public static bool Taking; /// Контейнер, который осматривает крыса, и до какого времени после закрытия его не разрушать. static Vector3i _sparePos; static float _spareUntil; /// Досягаемость контейнера от крысы, м. const float Reach = 2.5f; /// Мешок - не дальше стольких градусов от луча камеры (мешок мал, точно навести трудно). const float BagAimDeg = 25f; /// Открытый на просмотр мешок - для щелчков по ячейкам и «Взять всё». static EntityLootContainer _viewBag; /// Кто осматривает - для лога (у волка тот же обыск). static string Who => Possession.Zombie != null && WolfControl.Is(Possession.Zombie) ? "волк" : "крыса"; /// Мешок перед крысой: ближе Reach к ней и ближе всех к лучу камеры, не дальше BagAimDeg. public static EntityLootContainer FindBag(World _world, EntityPlayerLocal _player, EntityAlive _rat) { Transform cam = _player.cameraTransform; if (cam == null) { return null; } Vector3 from = cam.position + Origin.position; EntityLootContainer best = null; float bestDeg = BagAimDeg; for (int i = 0; i < _world.Entities.list.Count; i++) { if (!(_world.Entities.list[i] is EntityLootContainer bag) || bag.IsMarkedForUnload() || bag.bag == null || (bag.position - _rat.position).sqrMagnitude > Reach * Reach) { continue; } float deg = Vector3.Angle(cam.forward, bag.position + Vector3.up * 0.2f - from); if (deg < bestDeg) { bestDeg = deg; best = bag; } } return best; } /// Контейнер перед крысой по лучу камеры: блок с хранилищем ближе Reach к крысе; иначе null. public static TEFeatureStorage Find(World _world, EntityPlayerLocal _player, EntityAlive _rat, out Vector3i _pos, out BlockValue _bv) { _pos = Vector3i.zero; _bv = BlockValue.Air; Camera cam = _player.playerCamera; if (cam == null) { return null; } Vector3 eye = _rat.getHeadPosition(); Vector3 from = cam.transform.position + Origin.position; float dist = Vector3.Distance(from, eye) + Reach + 1f; Ray ray = new Ray(from, cam.transform.forward); if (!Voxel.Raycast(_world, ray, dist, -555266077, 69, 0f)) { return null; } WorldRayHitInfo hit = Voxel.voxelRayHitInfo; if ((hit.hit.pos - eye).sqrMagnitude > Reach * Reach) { return null; } Vector3i pos = hit.hit.blockPos; BlockValue bv = hit.hit.blockValue; Block block = bv.Block; if (block == null || bv.isair) { return null; } if (bv.ischild && block.multiBlockPos != null) { pos = block.multiBlockPos.GetParentPos(pos, bv); bv = _world.GetBlock(pos); } TileEntity te = _world.GetTileEntity(pos); TEFeatureStorage st = te != null ? te.GetSelfOrFeature() : null; if (st == null) { return null; } _pos = pos; _bv = bv; return st; } /// Каждый кадр одержимости крысой: подсказка и E - открыть. true - нажатие E ушло на контейнер или /// мешок (волку тогда не открывать свою сумку). public static bool Tick(World _world, EntityPlayerLocal _player, EntityAlive _rat, bool _activate, bool _take = false) { LocalPlayerUI ui = _player.PlayerUI; if (ui == null || Viewing || ui.windowManager.IsModalWindowOpen()) { return false; } PlayerActionsLocal input = _player.playerInput; string key = input.Activate.GetBindingXuiMarkupString() + input.PermanentActions.Activate.GetBindingXuiMarkupString(); TEFeatureStorage st = Find(_world, _player, _rat, out Vector3i pos, out BlockValue bv); if (st == null) { EntityLootContainer bag = FindBag(_world, _player, _rat); if (bag == null) { return false; } XUiC_InteractionPrompt.SetText(ui, string.Format(Localization.Get("necroPossessSearchBag"), key, bag.LocalizedEntityName)); if (!_activate) { return false; } Viewing = true; Taking = _take; _viewBag = bag; XUiC_InteractionPrompt.SetText(ui, null); _lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) }); _lockRequest.Invoke(LockManager.Instance, new object[] { bag, (ushort)0 }); Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает мешок " + EntityClass.GetEntityClassName(bag.entityClass) + " (" + bag.entityId + ")"); return true; } string text = st.GetActivationText(_world, pos, bv, _rat, key, bv.Block.GetLocalizedBlockName()); if (!string.IsNullOrEmpty(text)) { XUiC_InteractionPrompt.SetText(ui, text); } if (!_activate) { return false; } bool locked = st.lockpickFeature != null && st.lockpickFeature.NeedsLockpicking() || st.lockFeature != null && st.lockFeature.IsLocked() && !st.lockFeature.IsUserAllowed(Platform.PlatformManager.InternalLocalUserIdentifier) || st.isJammed; if (locked) { Audio.Manager.BroadcastPlayByLocalPlayer(pos.ToVector3() + Vector3.one * 0.5f, "Misc/locked"); return true; } Viewing = true; Taking = _take; _sparePos = pos; _spareUntil = float.MaxValue; XUiC_InteractionPrompt.SetText(ui, null); // Через отражение: у LockRequestLocal есть перегрузка с ReadOnlySpan, которой нет в справочных сборках проекта. _lockRequest = _lockRequest ?? AccessTools.Method(typeof(LockManager), nameof(LockManager.LockRequestLocal), new[] { typeof(ILockTarget), typeof(ushort) }); _lockRequest.Invoke(LockManager.Instance, new object[] { st, (ushort)0 }); Debug.Log("[NecromancerTome] RatLoot: " + Who + " осматривает " + bv.Block.GetBlockName() + " " + pos); return true; } static System.Reflection.MethodInfo _lockRequest; /// Отказ «рюкзак полон». public static void Deny() { LocalPlayerUI ui = LocalPlayerUI.GetUIForPrimaryPlayer(); if (ui?.entityPlayer != null) { GameManager.ShowTooltip(ui.entityPlayer, Localization.Get("xuiInventoryFullForPickup"), string.Empty, "ui_denied"); } } /// Выход из одержимости при открытом окне - закрыть его. public static void CloseIfViewing() { if (!Viewing) { return; } LocalPlayerUI ui = LocalPlayerUI.GetUIForPrimaryPlayer(); ui?.windowManager.CloseAllOpenModalWindows(); Viewing = false; _viewBag = null; _spareUntil = Time.time + 5f; } static void LockSlots(XUiC_LootWindow _w, bool _lock) { LockSlots(_w?.lootContainer, _lock); } static void LockSlots(XUiC_ItemStackGrid grid, bool _lock) { if (grid?.itemControllers == null) { return; } foreach (XUiC_ItemStack s in grid.itemControllers) { if (s != null) { s.IsLocked = _lock; } } } [HarmonyPatch(typeof(XUiC_LootWindowGroup), nameof(XUiC_LootWindowGroup.openContainer))] public static class Patch_openContainer { public static void Postfix(XUiC_LootWindowGroup __instance) { if (!Viewing) { return; } if (!Taking) { __instance.xui.playerUI.windowManager.Close("backpack"); // у волка рюкзак показывает его сумку (WolfBagBackpack) } LockSlots(__instance.lootWindow, true); } } /// Контейнеры с destroy_on_close (loot.xml: «empty» - 94 вида, мусор, кучи, холодильники, рюкзаки...; /// «true» - аэродроп и коробки Twitch, это сущности, крыса их не открывает) разрушаются при снятии замка /// после закрытия окна. Осмотр крысой их не разрушает (указание 08.10). [HarmonyPatch(typeof(GameManager), nameof(GameManager.CheckDestroyTileEntity))] public static class Patch_CheckDestroyTileEntity { public static bool Prefix(Vector3i _blockPos) { if (_blockPos != _sparePos || Time.time > _spareUntil) { return true; } _spareUntil = 0f; Debug.Log("[NecromancerTome] RatLoot: " + _blockPos + " после осмотра крысой не разрушен"); return false; } } /// Таймер первого обыска прерван - просмотра нет. [HarmonyPatch(typeof(XUiC_LootWindowGroup), nameof(XUiC_LootWindowGroup.openTimerClosedManually))] public static class Patch_openTimerClosedManually { public static void Postfix() { if (Viewing) { Viewing = false; _spareUntil = Time.time + 5f; } } } /// Отказ замка (контейнер занят другим) - просмотра нет. [HarmonyPatch(typeof(TEFeatureStorage), nameof(TEFeatureStorage.OnLockResponseLocal))] public static class Patch_OnLockResponseLocal { public static void Postfix(bool _success) { if (!_success && Viewing) { Viewing = false; _spareUntil = 0f; } } } [HarmonyPatch(typeof(XUiC_LootWindowGroup), nameof(XUiC_LootWindowGroup.OnClose))] public static class Patch_OnClose { public static void Postfix(XUiC_LootWindowGroup __instance) { LockSlots(__instance.lootWindow, false); // всегда: запертые ячейки не должны достаться персонажу if (Viewing) { Viewing = false; _spareUntil = Time.time + 5f; // снятие замка (и проверка разрушения) - следом, может прийти пакетом } } } /// Дальность окна - от крысы, не от тела: XUiC_LootWindow.Update меряет её от entityPlayer.position, /// поэтому на время его Update персонаж «стоит» на месте крысы (поле, ничего больше не трогается), а /// Finalizer возвращает позицию при любом исходе. (Тест 08.10: прежний вариант обнулял te - и закрытие окна /// по E/ESC падало в XUiC_LootWindow.OnClose, окно застревало и после выхода из одержимости.) [HarmonyPatch(typeof(XUiC_LootWindow), nameof(XUiC_LootWindow.Update))] public static class Patch_LootWindow_Update { public static void Prefix(XUiC_LootWindow __instance, out Vector3? __state) { __state = null; EntityPlayerLocal p = __instance.xui.playerUI.entityPlayer; EntityAlive rat = Possession.Zombie; if (!Viewing || p == null || rat == null) { return; } __state = p.position; p.position = rat.position; } public static void Finalizer(XUiC_LootWindow __instance, Vector3? __state) { if (__state.HasValue) { __instance.xui.playerUI.entityPlayer.position = __state.Value; } } } /// Окно мешка открылось на просмотр - рюкзак закрыть, ячейки запереть (как у ящика). [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))] public static class Patch_Bag_OnOpen { public static void Postfix(XUiC_BagStorageWindowGroup __instance) { if (!Viewing || _viewBag == null) { return; } if (!Taking) { __instance.xui.playerUI.windowManager.Close("backpack"); } LockSlots(__instance.containerWindow, true); } } [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnClose))] public static class Patch_Bag_OnClose { public static void Prefix(XUiC_BagStorageWindowGroup __instance) { LockSlots(__instance.containerWindow, false); // всегда: запертые ячейки не должны достаться персонажу if (Viewing && _viewBag != null) { Viewing = false; _viewBag = null; } } } /// Таймер обыска мешка прерван - просмотра нет. [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.CancelOpen))] public static class Patch_Bag_CancelOpen { public static void Postfix() { if (Viewing && _viewBag != null) { Viewing = false; _viewBag = null; } } } /// Замок мешка не дан (занят другим) - просмотра нет. [HarmonyPatch(typeof(Entity), nameof(Entity.OnLockResponseLocal))] public static class Patch_Entity_OnLockResponseLocal { public static void Postfix(Entity __instance, bool __result) { if (!__result && Viewing && _viewBag != null && __instance == _viewBag) { Viewing = false; _viewBag = null; } } } /// Дальность окна мешка - от крысы: проверка окна меряет её до entityPlayer, поэтому на время Update /// персонаж «стоит» на месте крысы, как у окна ящика. [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.Update))] public static class Patch_Bag_Update { public static void Prefix(XUiC_BagStorageWindowGroup __instance, out Vector3? __state) { __state = null; EntityPlayerLocal p = __instance.xui.playerUI.entityPlayer; EntityAlive rat = Possession.Zombie; if (!Viewing || _viewBag == null || p == null || rat == null) { return; } __state = p.position; p.position = rat.position; } public static void Finalizer(XUiC_BagStorageWindowGroup __instance, Vector3? __state) { if (__state.HasValue) { __instance.xui.playerUI.entityPlayer.position = __state.Value; } } } [HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveAll))] public static class Patch_MoveAll { public static bool Prefix(XUiC_ContainerStandardControls __instance) { if (!Viewing) { return true; } if (WolfBagButton(__instance, false)) { return false; } TEFeatureStorage te = __instance.GetParentByType()?.te; if (Taking && te != null) { WolfBag.TakeAll(te); } else if (Taking && _viewBag != null) { WolfBag.TakeAll(_viewBag); } else { Deny(); } return false; } } [HarmonyPatch(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.Sort))] public static class Patch_Sort { public static bool Prefix(XUiC_ContainerStandardControls __instance) { if (Viewing && WolfBagBackpack.On && __instance.GetParentByType() != null) { WolfBag.Sort(); } return !Viewing; } } /// Кнопки окна рюкзака, показывающего сумку волка: «Положить всё» / «Положить похожие» - в открытый /// ящик или мешок (как щелчок по ячейке сумки). True - кнопка сумки волка, обработана. static bool WolfBagButton(XUiC_ContainerStandardControls _c, bool _similarOnly) { if (!WolfBagBackpack.On || _c.GetParentByType() == null) { return false; } XUiC_LootWindow lw = _c.xui.GetChildByType(); TEFeatureStorage dst = lw != null && lw.ViewComponent.IsVisible ? lw.te : null; if (dst != null || _viewBag != null) { WolfBag.PutAll(dst, dst == null ? _viewBag : null, _similarOnly); } return true; } /// «Положить похожие» у сумки волка; прочие переносы и замок ячеек при обыске - выключены (ячейки окна - /// копии сумки, ванильный перенос их раздвоил бы). [HarmonyPatch(typeof(XUiC_ContainerStandardControls))] public static class Patch_OtherMoves { static System.Collections.Generic.IEnumerable TargetMethods() { yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillAndSmart)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveFillStacks)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveSmart)); yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.ToggleLockMode)); } public static bool Prefix(XUiC_ContainerStandardControls __instance, System.Reflection.MethodBase __originalMethod) { if (!Viewing) { return true; } if (__originalMethod.Name == nameof(XUiC_ContainerStandardControls.MoveFillAndSmart)) { WolfBagButton(__instance, true); } return false; } } /// Щелчок по запертой ячейке контейнера - отказ «рюкзак полон». [HarmonyPatch(typeof(XUiC_ItemStack), nameof(XUiC_ItemStack.Update))] public static class Patch_ItemStack_Update { public static void Postfix(XUiC_ItemStack __instance) { if (!Viewing || !__instance.isOver || __instance.ItemStack == null || __instance.ItemStack.IsEmpty()) { return; } // Ячейка сумки волка (окно рюкзака при обыске) - в ящик или мешок (указание 10.10). XUiC_Backpack bp = __instance.GetParentByType(); if (bp != null) { CursorControllerAbs c = __instance.xui.playerUI.CursorController; if (WolfBagBackpack.On && (c.GetMouseButtonDown(UICamera.MouseButton.LeftButton) || c.GetMouseButtonDown(UICamera.MouseButton.RightButton))) { int bi = bp.itemControllers != null ? System.Array.IndexOf(bp.itemControllers, __instance) : -1; XUiC_LootWindow lw = __instance.xui.GetChildByType(); TEFeatureStorage dst = lw != null && lw.ViewComponent.IsVisible ? lw.te : null; if (bi >= 0 && dst != null) { WolfBag.PutTo(dst, bi); } else if (bi >= 0 && _viewBag != null) { WolfBag.PutTo(_viewBag, bi); } } return; } // Ячейка контейнера-блока (XUiC_LootContainer) или мешка (XUiC_BagContainer), не рюкзака. XUiC_ItemStackGrid grid = (XUiC_ItemStackGrid)__instance.GetParentByType() ?? __instance.GetParentByType(); if (grid == null) { return; } CursorControllerAbs cur = __instance.xui.playerUI.CursorController; if (cur.GetMouseButtonDown(UICamera.MouseButton.LeftButton) || cur.GetMouseButtonDown(UICamera.MouseButton.RightButton)) { TEFeatureStorage te = __instance.GetParentByType()?.te; int idx = grid.itemControllers != null ? System.Array.IndexOf(grid.itemControllers, __instance) : -1; if (Taking && te != null && idx >= 0) { WolfBag.TakeFrom(te, idx); } else if (Taking && _viewBag != null && grid is XUiC_BagContainer && idx >= 0) { WolfBag.TakeFrom(_viewBag, idx); } else { Deny(); } } } } } }