using System.Collections.Generic; using System.IO; using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Сумка одержимого лютоволка на 3 ячейки (решено 09.10, BACKLOG «Медведь и лютоволк в одержимости»): /// - волк лутает: окно контейнера то же, что у крысы (RatLoot, рюкзак персонажа не показывается), /// но щелчок по предмету (и «Взять всё») кладёт его в сумку волка; места нет - «Сумка полна»; /// - выгрузка - принести к телу: рядом с телом E перекладывает сумку в рюкзак персонажа, что не /// влезло - мешком на землю рядом; /// - волк погиб или отозван - сумка выпадает мешком на месте; при отзыве (указание 09.10) на карте метка /// «Сумка волка», чтобы за ним прийти; мешок подобран или истлел - метка снята (Patch_removeBackpack); /// - «багажник» (указание 09.10: «почти как у пространственного хранилища»): E не на контейнер и не у тела - /// окно сумки, то же ванильное окно, что у браслета (XUiC_BagStorageWindowGroup, шаблон roboticDrone), но /// без рюкзака персонажа: внутри перекладывать, вытащить за окно - выбросить у волка (Patch_GetDropPosition); /// - мешки с зомби волк лутает так же, как ящики (RatLoot, TakeFrom(EntityLootContainer)); /// - тест 10.10: «волк - это ещё одно пространственное хранилище: берёт игрок, но положить может только в эти /// 3 ячейки» - при обыске рядом с ящиком окно рюкзака показывает сумку волка (WolfBagBackpack: первые 3 /// ячейки, остальные скрыты, ячейки заперты - перекладка щелчком: по ящику - в сумку, по сумке - в ящик, /// PutTo), E на предмет на земле кладёт его в сумку (TryPickup), Tab в волке - окно сумки, не рюкзак /// персонажа (Possession: windowpaging закрывается). /// /// ХРАНЕНИЕ. Сумку сущности игра не сохраняет (Entity.bag нет в Entity.Write), поэтому она своя. /// Упаковка - та же, что у пространственного хранилища (SpatialVaultPersistence.BuildPayload/ParsePayload, /// проверена на сейвах 3.2 и 3.3), но в ОТДЕЛЬНОМ файле папки сейва, как у Чертогов разума /// (NecromancerTome_palaces.txt): файл игрока не меняется (сейвы - главный риск мода). Пишется сразу /// при каждом изменении - переживает и сохранение, и вылет. Волк у игрока один (один спутник за раз), /// поэтому сумка одна, без привязки к номеру сущности. /// public static class WolfBag { public const int Slots = 3; const string FileName = "NecromancerTome_wolfbag.bin"; public const float UnloadRange = 3f; /// Метка выпавшей сумки: имя - ключ локализации, по нему и находим свою метку среди меток игрока. public const string WaypointName = "necroWolfBagWaypoint"; public const string WaypointIcon = "ui_game_symbol_backpack"; /// Открыто окно сумки («багажник»). public static bool WindowOpen; /// Счётчик изменений сумки - окно рюкзака перерисовывает её по нему. public static int Version; /// Предмет на земле - не дальше стольких метров от волка и градусов от луча камеры. const float PickupReach = 2.5f; const float PickupAimDeg = 25f; static Bag _bag; static string _dir; static string FilePath => Path.Combine(GameIO.GetSaveGameDir(), FileName); /// Сумка текущего сейва (из файла при первом обращении в этом сейве). public static Bag Get() { string dir = GameIO.GetSaveGameDir(); if (_bag != null && _dir == dir) { return _bag; } _dir = dir; _bag = null; try { if (File.Exists(FilePath)) { _bag = SpatialVaultPersistence.ParsePayload(File.ReadAllBytes(FilePath)); } } catch (System.Exception e) { Debug.LogWarning("[NecromancerTome] WolfBag: файл сумки не прочитан (" + e.Message + ") - оставлен как есть, сумка пустая"); _bag = new Bag(new Vector2i(Slots, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null); return _bag; } if (_bag == null) { _bag = new Bag(new Vector2i(Slots, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null); } else if (_bag.SlotCount < Slots) { _bag.Resize(new Vector2i(Slots, 1)); } Debug.Log("[NecromancerTome] WolfBag: сумка загружена - " + Describe()); return _bag; } /// Сумка, куда идёт лут сейчас (13.10): одержимого зомби (ZombieBag), если он с сумкой, иначе волка. /// Обыск, подбор с земли, окно рюкзака при обыске и подсказки - общие, разница только здесь. public static Bag Target => ZombieBag.Active ? ZombieBag.Current : Get(); public static int TargetSlots => ZombieBag.Active ? ZombieBag.Current.SlotCount : Slots; static void SaveTarget() { if (ZombieBag.Active) { Version++; ZombieBag.SaveNow(); return; } Save(); } static void Save() { Version++; try { if (_bag == null || _bag.IsEmpty()) { if (File.Exists(FilePath)) { File.Delete(FilePath); } return; } File.WriteAllBytes(FilePath, SpatialVaultPersistence.BuildPayload(_bag)); } catch (System.Exception e) { Debug.LogWarning("[NecromancerTome] WolfBag: сумка не записана - " + e.Message); } } public static string Describe() { Bag b = Target; List parts = new List(); foreach (ItemStack s in b.ItemGrid.items) { if (s != null && !s.IsEmpty()) { parts.Add(s.itemValue.ItemClass.GetLocalizedItemName() + (s.count > 1 ? " x" + s.count : "")); } } return parts.Count == 0 ? Localization.Get("necroWolfBagEmpty") : string.Join(", ", parts); } static void Show(string _key) { EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer(); if (p != null) { GameManager.ShowTooltip(p, string.Format(Localization.Get(ZombieBag.Active ? _key.Replace("necroWolfBag", "necroZombieBag") : _key), Describe(), Target.GetUsedSlotCount(), TargetSlots)); } } /// Переложить ячейку контейнера в сумку. Без раздвоения: сперва в неполные стопки, остаток - /// в пустую ячейку; в контейнере остаётся ровно то, что не влезло. public static bool TakeFrom(TEFeatureStorage _te, int _idx) { ItemStack rest = Take(_te.ItemGrid.GetItem(_idx)); if (rest == null) { return false; } _te.UpdateSlot(_idx, rest); _te.SetModified(); _te.Parent?.NotifyListeners(); // окно контейнера перерисует ячейку (как ванильная сортировка) return true; } /// То же из мешка, упавшего с зомби. Окно мешка перерисуется по событию сумки (SetSlot). public static bool TakeFrom(EntityLootContainer _src, int _idx) { if (_idx >= _src.bag.SlotCount) { return false; } ItemStack rest = Take(_src.bag.ItemGrid.items[_idx]); if (rest == null) { return false; } _src.bag.SetSlot(_idx, rest); _src.OnBagModified(); // на клиенте - отправить сумку мешка серверу, как делает окно return true; } /// Переложить стопку в сумку; что осталось в источнике (пустая стопка, если всё), или null - не влезло. static ItemStack Take(ItemStack src) { if (src == null || src.IsEmpty()) { return null; } Bag b = Target; ItemStack moving = src.Clone(); int before = moving.count; b.TryStackItem(0, moving); if (moving.count > 0 && b.AddItem(moving.Clone())) { moving.count = 0; } int moved = before - moving.count; if (moved <= 0) { Audio.Manager.PlayInsidePlayerHead("ui_denied"); Show("necroWolfBagFull"); return null; } ItemStack rest = src.Clone(); rest.count = moving.count; SaveTarget(); Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1"); Show("necroWolfBagTook"); Debug.Log("[NecromancerTome] WolfBag: в сумку " + src.itemValue.ItemClass.GetItemName() + " x" + moved + " - " + Describe()); return rest.count > 0 ? rest : ItemStack.Empty.Clone(); } /// Ячейку сумки - в ящик (щелчок по сумке в окне рюкзака при обыске, тест 10.10: «выложить в ящик не /// получается - это неправильно»). Сперва в неполные стопки, остаток - в пустые ячейки; что не влезло - /// остаётся в сумке. public static bool PutTo(TEFeatureStorage _te, int _idx) { ItemStack[] g = _te.ItemGrid.items; List changed = new List(); if (!Give(_idx, g, changed)) { return false; } foreach (int i in changed) { _te.UpdateSlot(i, g[i].Clone()); } _te.SetModified(); _te.Parent?.NotifyListeners(); return true; } /// То же в мешок с зомби. public static bool PutTo(EntityLootContainer _dst, int _idx) { ItemStack[] g = _dst.bag.ItemGrid.items; List changed = new List(); if (!Give(_idx, g, changed)) { return false; } foreach (int i in changed) { _dst.bag.SetSlot(i, g[i].Clone()); } _dst.OnBagModified(); return true; } /// Кнопки окна сумки при обыске (указание 12.10): «Положить всё» и «Положить похожие» - в открытый /// ящик или мешок; похожие - только то, что в контейнере уже лежит. public static void PutAll(TEFeatureStorage _te, EntityLootContainer _bag, bool _similarOnly) { bool any = false; ItemStack[] slots = Target.ItemGrid.items; for (int i = 0; i < slots.Length; i++) { if (slots[i] == null || slots[i].IsEmpty()) { continue; } ItemStack[] dst = _te != null ? _te.ItemGrid.items : _bag?.bag.ItemGrid.items; if (dst == null || (_similarOnly && System.Array.FindIndex(dst, s => s != null && !s.IsEmpty() && s.itemValue.type == slots[i].itemValue.type) < 0)) { continue; } any |= _te != null ? PutTo(_te, i) : PutTo(_bag, i); } if (!any) { Audio.Manager.PlayInsidePlayerHead("ui_denied"); } } /// «Сортировать» - по самой сумке. public static void Sort() { Target.ItemGrid.Sort(); SaveTarget(); } /// Переложить ячейку сумки в массив ячеек контейнера (сами ячейки меняются, номера - в _changed). static bool Give(int _idx, ItemStack[] _g, List _changed) { Bag b = Target; ItemStack src = _idx < b.SlotCount ? b.ItemGrid.items[_idx] : null; if (src == null || src.IsEmpty()) { return false; } int left = src.count; int max = src.itemValue.ItemClass?.Stacknumber.Value ?? 1; for (int i = 0; i < _g.Length && left > 0 && max > 1; i++) { if (_g[i] != null && !_g[i].IsEmpty() && _g[i].itemValue.type == src.itemValue.type && _g[i].count < max) { int n = Mathf.Min(left, max - _g[i].count); _g[i] = _g[i].Clone(); _g[i].count += n; left -= n; _changed.Add(i); } } for (int i = 0; i < _g.Length && left > 0; i++) { if (_g[i] == null || _g[i].IsEmpty()) { _g[i] = new ItemStack(src.itemValue.Clone(), left); left = 0; _changed.Add(i); } } int moved = src.count - left; if (moved <= 0) { Audio.Manager.PlayInsidePlayerHead("ui_denied"); return false; } string name = src.itemValue.ItemClass?.GetItemName(); // до SetSlot: src - сама ячейка сумки, очистится ItemStack rest = src.Clone(); rest.count = left; b.SetSlot(_idx, left > 0 ? rest : ItemStack.Empty.Clone()); SaveTarget(); Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1"); Debug.Log("[NecromancerTome] WolfBag: из сумки в контейнер " + name + " x" + moved + " - " + Describe()); return true; } /// E на предмет, лежащий на земле, - в сумку (как игрок подбирает: что не влезло, остаётся лежать). /// True - предмет перед волком был (взят или «сумка полна»). public static bool TryPickup(World _world, EntityPlayerLocal _player, EntityAlive _wolf) { Transform cam = _player.cameraTransform; if (cam == null || _world.IsRemote()) { return false; } Vector3 from = cam.position + Origin.position; EntityItem best = null; float bestDeg = PickupAimDeg; for (int i = 0; i < _world.Entities.list.Count; i++) { if (!(_world.Entities.list[i] is EntityItem it) || it.IsDead() || it.IsMarkedForUnload() || it.itemStack == null || it.itemStack.IsEmpty() || !it.CanCollect() || (it.position - _wolf.position).sqrMagnitude > PickupReach * PickupReach) { continue; } float deg = Vector3.Angle(cam.forward, it.position - from); if (deg < bestDeg) { bestDeg = deg; best = it; } } if (best == null) { return TryPickupBlock(_world, _player, cam, from, _wolf); } ItemStack rest = Take(best.itemStack); if (rest == null) { return true; } if (rest.IsEmpty()) { _world.RemoveEntity(best.entityId, EnumRemoveEntityReason.Killed); } else { best.itemStack.count = rest.count; } return true; } /// E на подбираемую «природную мелочь» мира - камешек rock01 и т. п. (тест 13.10: «лутающие одержимые и /// питомцы не могут подбирать предметы, например камни»): блок с CanPickup, что персонаж поднимает по E, - в /// сумку (что выдаёт - Block.OnBlockPickedUp, как у игры), блок убрать. Только IsTerrainDecoration: CanPickup /// есть и у построек игрока (каркасы, фонари, котелки) - их по E не утаскиваем. Чужой участок - как у игры /// (CanPickupBlockAt). Ближайший к лучу камеры в PickupReach от того, кто подбирает. static bool TryPickupBlock(World _world, EntityPlayerLocal _player, Transform _cam, Vector3 _from, EntityAlive _who) { Vector3i c = World.worldToBlockPos(_who.position); int r = Mathf.CeilToInt(PickupReach); Vector3i best = Vector3i.zero; BlockValue bestBv = BlockValue.Air; float bestDeg = PickupAimDeg; for (int x = -r; x <= r; x++) for (int y = -1; y <= 2; y++) for (int z = -r; z <= r; z++) { Vector3i p = new Vector3i(c.x + x, c.y + y, c.z + z); BlockValue bv = _world.GetBlock(p); if (bv.isair || bv.ischild) { continue; } Block b = bv.Block; if (b == null || !b.CanPickup || !b.IsTerrainDecoration) { continue; } Vector3 at = p.ToVector3() + new Vector3(0.5f, 0.3f, 0.5f); if ((at - _who.position).sqrMagnitude > PickupReach * PickupReach) { continue; } float deg = Vector3.Angle(_cam.forward, at - _from); if (deg < bestDeg) { bestDeg = deg; best = p; bestBv = bv; } } if (bestBv.isair || !_world.CanPickupBlockAt(best, GameManager.Instance.GetPersistentLocalPlayer())) { return false; } ItemStack st = bestBv.Block.OnBlockPickedUp(_world, best, bestBv, _player.entityId); ItemStack rest = Take(st); if (rest == null) { return true; } if (rest.IsEmpty()) { _world.SetBlockRPC(best, BlockValue.Air); Debug.Log("[NecromancerTome] WolfBag: поднят блок " + bestBv.Block.GetBlockName() + " " + best); } return true; } /// «Взять всё» - по ячейкам, пока есть место. public static void TakeAll(TEFeatureStorage _te) { bool any = false; for (int i = 0; i < _te.ItemGrid.items.Length; i++) { if (!_te.ItemGrid.items[i].IsEmpty() && Target.CanTakeItem(_te.ItemGrid.items[i])) { any |= TakeFrom(_te, i); } } if (!any) { Audio.Manager.PlayInsidePlayerHead("ui_denied"); Show("necroWolfBagFull"); } } /// «Взять всё» из мешка - по ячейкам, пока есть место. public static void TakeAll(EntityLootContainer _src) { bool any = false; ItemStack[] slots = _src.bag.ItemGrid.items; for (int i = 0; i < slots.Length; i++) { if (!slots[i].IsEmpty() && Target.CanTakeItem(slots[i])) { any |= TakeFrom(_src, i); } } if (!any) { Audio.Manager.PlayInsidePlayerHead("ui_denied"); Show("necroWolfBagFull"); } } /// «Багажник»: окно сумки, как у пространственного браслета (шаблон дрона), без рюкзака персонажа. /// Перекладка внутри пишется сразу (Save на каждое изменение и на закрытие). public static void OpenWindow(EntityPlayerLocal _player, EntityAlive _wolf) { LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player); if (ui == null) { return; } WindowOpen = true; XUiC_BagStorageWindowGroup.Open(ui.xui, _wolf, Get(), LootContainer.GetLootContainer("roboticDrone"), Localization.Get("necroWolfBagWindowTitle"), Save, () => { WindowOpen = false; Save(); }); Debug.Log("[NecromancerTome] WolfBag: окно сумки - " + Describe()); } /// Выход из одержимости при открытом окне сумки - закрыть его. public static void CloseWindow() { if (WindowOpen) { LocalPlayerUI.GetUIForPrimaryPlayer()?.windowManager.CloseAllOpenModalWindows(); WindowOpen = false; } } /// E рядом с телом: сумка - в рюкзак, что не влезло - мешком у тела. public static bool TryUnload(EntityPlayerLocal _body, EntityAlive _wolf) { if (_body == null || (_body.position - _wolf.position).sqrMagnitude > UnloadRange * UnloadRange) { return false; } Bag b = Get(); if (b.IsEmpty()) { Show("necroWolfBagShow"); return true; } List left = new List(); ItemStack[] slots = b.ItemGrid.items; for (int i = 0; i < slots.Length; i++) { ItemStack s = slots[i]?.Clone(); if (s == null || s.IsEmpty()) { continue; } _body.bag.TryStackItem(0, s); if (s.count > 0 && _body.bag.AddItem(s.Clone())) { s.count = 0; } if (s.count > 0) { left.Add(s); } } b.Clear(); Save(); if (left.Count > 0) { Drop(_body.position + _body.GetForwardVector() * 0.8f, left); } Audio.Manager.PlayInsidePlayerHead("UseActions/takeall1"); GameManager.ShowTooltip(_body, Localization.Get(left.Count > 0 ? "necroWolfBagUnloadedPart" : "necroWolfBagUnloaded")); Debug.Log("[NecromancerTome] WolfBag: выгружена у тела" + (left.Count > 0 ? ", не влезло " + left.Count + " - мешком" : "")); return true; } /// Волк погиб или отозван - сумка мешком на месте. public static void DropFrom(EntityAlive _wolf, string _why) { if (_wolf == null || !WolfControl.Is(_wolf)) { return; } Bag b = Get(); if (b.IsEmpty()) { return; } List items = new List(); foreach (ItemStack s in b.ItemGrid.items) { if (s != null && !s.IsEmpty()) { items.Add(s.Clone()); } } b.Clear(); Save(); Drop(_wolf.position, items); if (_why == "отозван") { AddWaypoint(_wolf.position); } Debug.Log("[NecromancerTome] WolfBag: волк " + _why + " - сумка выпала мешком (" + items.Count + ")"); } /// Метка на карте и компасе - как игра ставит метку транспорта (XUiC_MapArea.RefreshVehiclePositionWaypoint), /// но обычная, не «последнее место сущности»: номер мешка после перезахода другой. Метка - в списке меток игрока, /// сохраняется игрой вместе с ним; удалить можно и вручную. public static void AddWaypoint(Vector3 _at, string _name = WaypointName) { EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer(); if (p?.Waypoints == null) { return; } Waypoint w = new Waypoint(); w.pos = World.worldToBlockPos(_at); w.icon = WaypointIcon; w.ownerId = Platform.PlatformManager.InternalLocalUserIdentifier; w.name.Update(_name, Platform.PlatformManager.MultiPlatform.User.PlatformUserId); w.bUsingLocalizationId = true; p.Waypoints.Collection.Add(w); w.navObject = NavObjectManager.Instance.RegisterNavObject("waypoint", w.pos.ToVector3(), w.icon); w.navObject.IsActive = false; w.navObject.name = w.name.Text; w.navObject.usingLocalizationId = true; Debug.Log("[NecromancerTome] WolfBag: метка сумки на карте " + w.pos); } /// Мешок исчез (опустел или истлел) - снять метку сумки, стоящую рядом с ним. public static void RemoveWaypointAt(Vector3 _at) { EntityPlayerLocal p = GameManager.Instance?.World?.GetPrimaryPlayer(); if (p?.Waypoints == null) { return; } List list = p.Waypoints.Collection.list; for (int i = list.Count - 1; i >= 0; i--) { Waypoint w = list[i]; if ((w.name.Text == WaypointName || w.name.Text == ZombieBag.WaypointName) && (w.pos.ToVector3() - _at).sqrMagnitude < 3f * 3f) { p.Waypoints.Collection.Remove(w); if (w.navObject != null) { NavObjectManager.Instance.UnRegisterNavObject(w.navObject); } Debug.Log("[NecromancerTome] WolfBag: мешок сумки подобран - метка снята " + w.pos); } } } /// Мешок с предметами на земле (и для сумки одержимого зомби, ZombieBag). public static void DropSack(Vector3 _at, List _items) => Drop(_at, _items); static void Drop(Vector3 _at, List _items) { GameManager.Instance.DropContentInLootContainerServer(-1, "DroppedLootContainer", _at + Vector3.up * 0.5f, _items.ToArray()); } /// Выход из мира - забыть (следующий сейв прочитает свой файл). public static void Forget() { _bag = null; _dir = null; } } /// Мешок исчез (пустой после обыска - «empty», или истлел - «old») - снять метку сумки волка рядом. [HarmonyPatch(typeof(EntityContainerAbs), nameof(EntityContainerAbs.removeBackpack))] public static class Patch_EntityContainerAbs_removeBackpack_WolfBag { public static void Postfix(EntityContainerAbs __instance) { if (__instance is EntityLootContainer) { WolfBag.RemoveWaypointAt(__instance.position); } } } /// «Багажник» без рюкзака персонажа: окно мешка открывает его само (OnOpen), закрываем сразу же. [HarmonyPatch(typeof(XUiC_BagStorageWindowGroup), nameof(XUiC_BagStorageWindowGroup.OnOpen))] public static class Patch_XUiC_BagStorageWindowGroup_OnOpen_WolfBag { public static void Postfix(XUiC_BagStorageWindowGroup __instance) { bool wolf = (WolfBag.WindowOpen || ZombieBag.WindowOpen) && Possession.Active; if (wolf) { __instance.xui.playerUI.windowManager.Close("backpack"); } // «Взять всё» / «Взять похожие» кладут в рюкзак тела, а оно далеко - у волка кнопок нет; у дрона и браслета - вернуть XUiC_ContainerStandardControls c = __instance.GetChildByType()?.standardControls; foreach (string id in WolfBagControls.Hidden) { XUiController b = c?.GetChildById(id); if (b?.ViewComponent != null) { b.ViewComponent.IsVisible = !wolf; } } } } /// Окно сумки волка: работает только «Сортировать» (по самой сумке, пишется через onModified). /// Перенос в рюкзак (кнопки и клавиша R) и замки ячеек (нужны лишь для переноса) - выключены. public static class WolfBagControls { public static readonly string[] Hidden = { "btnMoveAll", "btnMoveFillAndSmart", "btnMoveFillStacks", "btnMoveSmart" }; public static bool Blocked(XUiController _c) { return (WolfBag.WindowOpen || ZombieBag.WindowOpen) && Possession.Active && _c?.GetParentByType() != null; } } [HarmonyPatch(typeof(XUiC_ContainerStandardControls))] public static class Patch_ContainerStandardControls_Move_WolfBag { static IEnumerable TargetMethods() { yield return AccessTools.Method(typeof(XUiC_ContainerStandardControls), nameof(XUiC_ContainerStandardControls.MoveAll)); 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) { return !WolfBagControls.Blocked(__instance); } } /// Кнопка замка видна по привязке {hasslotlocks} - у волка её нет. [HarmonyPatch(typeof(XUiC_BagContainer), nameof(XUiC_BagContainer.GetBindingValueInternal))] public static class Patch_BagContainer_Binding_WolfBag { public static void Postfix(XUiC_BagContainer __instance, ref string _value, string _bindingName, ref bool __result) { if (_bindingName == "hasslotlocks" && WolfBagControls.Blocked(__instance)) { _value = "false"; __result = true; } } } /// Предмет, вытащенный за окно сумки волка, падает у волка, а не у тела (XUiM_PlayerInventory.DropItem /// берёт место у игрока - GetDropPosition). [HarmonyPatch(typeof(EntityPlayerLocal), nameof(EntityPlayerLocal.GetDropPosition))] public static class Patch_EntityPlayerLocal_GetDropPosition_WolfBag { public static void Postfix(EntityPlayerLocal __instance, ref Vector3 __result) { EntityAlive wolf = Possession.Zombie; // И из сумки одержимого зомби (тест 13.10: «выкидываю камень - на землю не падает» - падал у тела). if ((WolfBag.WindowOpen || ZombieBag.WindowOpen) && Possession.Active && __instance == Possession.Player && wolf != null) { __result = wolf.position + wolf.GetForwardVector() * 0.8f + Vector3.up * 0.5f; } } } /// Окно рюкзака при обыске волком показывает сумку волка (указание 10.10). Рюкзак персонажа при этом /// не читается и не пишется: UpdateBackend пропущен - в сейв игрока сумка не попадёт. public static class WolfBagBackpack { public static bool On => RatLoot.Viewing && RatLoot.Taking && Possession.Active; static int _seen = -1; static readonly List _hidden = new List(); public static ItemStack[] Slots(int _n) { ItemStack[] a = new ItemStack[_n]; ItemStack[] b = WolfBag.Target.ItemGrid.items; for (int i = 0; i < _n; i++) { a[i] = i < b.Length && b[i] != null ? b[i].Clone() : ItemStack.Empty.Clone(); } return a; } public static void Tick(XUiC_Backpack _bp) { if (_seen != WolfBag.Version || _hidden.Count == 0) { _seen = WolfBag.Version; _bp.RefreshBackpackSlots(); } XUiC_ItemStack[] c = _bp.itemControllers; for (int i = 0; c != null && i < c.Length; i++) { c[i].IsLocked = true; if (i >= WolfBag.TargetSlots && c[i].ViewComponent.IsVisible) { c[i].ViewComponent.IsVisible = false; _hidden.Add(c[i]); } } } /// Обыск закончен: ячейки рюкзака снова видны и отперты, рюкзак - снова свой. public static void Unlock(XUiC_Backpack _bp) { foreach (XUiC_ItemStack s in _hidden) { if (s != null) { s.ViewComponent.IsVisible = true; } } bool was = _hidden.Count > 0 || _seen != -1; _hidden.Clear(); _seen = -1; if (_bp?.itemControllers == null || !was) { return; } foreach (XUiC_ItemStack s in _bp.itemControllers) { if (s != null) { s.IsLocked = false; } } _bp.RefreshBackpackSlots(); } } [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.GetSlots))] public static class Patch_Backpack_GetSlots_WolfBag { public static bool Prefix(XUiC_Backpack __instance, ref ItemStack[] __result) { if (!WolfBagBackpack.On) { return true; } __result = WolfBagBackpack.Slots(__instance.itemControllers?.Length ?? WolfBag.TargetSlots); return false; } } [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.UpdateBackend))] public static class Patch_Backpack_UpdateBackend_WolfBag { public static bool Prefix() { return !WolfBagBackpack.On; } } [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.Update))] public static class Patch_Backpack_Update_WolfBag { static bool _was; public static void Postfix(XUiC_Backpack __instance) { bool on = WolfBagBackpack.On; if (on) { WolfBagBackpack.Tick(__instance); } else if (_was) { WolfBagBackpack.Unlock(__instance); } _was = on; } } [HarmonyPatch(typeof(XUiC_Backpack), nameof(XUiC_Backpack.OnClose))] public static class Patch_Backpack_OnClose_WolfBag { public static void Postfix(XUiC_Backpack __instance) { WolfBagBackpack.Unlock(__instance); } } [HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.OnEntityDeath))] public static class Patch_EntityAlive_OnEntityDeath_WolfBag { public static void Prefix(EntityAlive __instance) { if (!__instance.world.IsRemote() && WolfControl.Is(__instance)) { WolfBag.DropFrom(__instance, "погиб"); } } } }