Files
necromants-tome-7d2d-3-2/HarmonySrc/BlockPickupPatch.cs
T
AlexCubeandClaude Opus 5 b6a892cadd Подбор кроватей: включены те, что становятся точкой возрождения
Исправление промаха по цели. Кровати класса SleepingBag (bed02*, bunkBed*) были
исключены из подбора НАМЕРЕННО, с формулировкой "это respawn-anchor блоки, не
декорация, трогать не просил" - и подбирались только декоративные
bedMadeNoFrame*/bedMessyNoFrame*.

Пользователь объяснил, что смысл подбора кроватей ровно обратный: "поглощённые
блоки сохраняют состояние, которое было до поглощения. Кровати до поглощения не
являются точкой возрождения. Точкой возрождения они могли бы стать при подборе".
То есть мы подбирали ровно те кровати, которые не нужны, и не подбирали те, ради
которых всё затевалось.

Проверено декомпиляцией BlockSleepingBag: PlaceBlock ставит игроку
selectedSpawnPointKey и запоминает BedrollPos - поставленная своими руками
кровать действительно становится точкой возрождения, а стоящая в POI нет.
Значит подбор и поглощение браслетом решают РАЗНЫЕ задачи.

Новых патчей не понадобилось, и это проверялось до правки: BlockSleepingBag
переопределяет только CanPlaceBlockAt, PlaceBlock, GetActivationText и
ЧЕТЫРЁХаргументный OnBlockActivated. Наши три патча висят на
HasBlockActivationCommands, GetBlockActivationCommands и ПЯТИаргументном
OnBlockActivated - ни одного из них он не перекрывает. С BlockCompositeTileEntity
(кулеры, коробки) в своё время так не вышло и понадобился отдельный набор.

Добавлены два префикса: bed02 и bunkBed. Полсотни цветовых вариантов наследуют
Class="SleepingBag" через Extends и покрываются ими же. bedroll НЕ включён:
собственный предмет игрока, ставится и снимается штатно, а bedrollPOI от него не
наследуется и точкой возрождения не является.

В ИГРЕ НЕ ПРОВЕРЕНО. Смотреть надо одно: становится ли поднятая и заново
поставленная кровать точкой возрождения на самом деле.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:21:36 +03:00

228 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// BACKLOG.md item 9 (dictated 2026-08-29, implemented same day "без вопросов" per user
/// request, clarified same day to drop the land-claim requirement entirely). Lets specific
/// decorative/prop blocks be picked back up as an item via hold-E + a hand icon + a progress
/// timer, the same VISUAL mechanic vanilla workstations (workbench/forge/etc.) already use
/// when placed inside your own land claim - but WITHOUT the land-claim requirement, works
/// anywhere on the map, per the user's explicit clarification.
///
/// RESEARCH FIRST, not guessed (decompiled Assembly-CSharp's Block/BlockWorkstation/
/// BlockCompositeTileEntity/BlockTrunkTip classes directly):
///
/// - The actual "hold E, see hand icon + timer, get the item back" mechanic is a GENERIC
/// pair of methods already on the base `Block` class itself, not something
/// BlockWorkstation invented: `Block.takeItemWithTimer(...)` (instance) calls the static
/// `Block.TakeItemWithTimer(pos, blockValue, player, delaySeconds, canTakeCallback)`, which
/// opens the real timer UI (XUiC_Timer.OpenTimer) and, on completion, converts the block to
/// an item, adds it to inventory (or drops it if full), and clears the block - all engine-
/// native, nothing reimplemented here. BlockWorkstation's own "take" activation command is
/// just ONE caller of this generic method, gated behind
/// `_world.IsMyLandProtectedBlock(...) &amp;&amp; tileEntityWorkstation.IsPlayerPlaced` (that IS
/// real land-claim gating in vanilla, confirmed - the backlog's original worry about
/// needing Harmony either way was right) - this patch calls the SAME generic
/// TakeItemWithTimer directly, deliberately WITHOUT that land-claim check, per the user's
/// own clarification.
/// - Which "take" appears on a block at all is decided by `Block.GetBlockActivationCommands`/
/// `HasBlockActivationCommands`/`OnBlockActivated(string,...)` - all three are `virtual` on
/// the base `Block` class, so a plain undecorated block (no Class= override in XML) runs the
/// base implementation and can be patched there directly. But several candidate blocks use a
/// DIFFERENT C# class that overrides all three (confirmed by decompiling it) -
/// `BlockCompositeTileEntity` (used by the water cooler/cardboard box below) - so those need
/// their own separate patches on that type; a patch on the base `Block` type alone would
/// never run for them (Harmony patches the actual method that executes via virtual dispatch,
/// not every subclass "logically implementing the same slot").
///
/// TARGET BLOCKS - best-guess mapping from the user's four Russian category names to real
/// Data/Config/blocks.xml block names (checked directly, matched by name PREFIX since most
/// categories have many color/variant blocks) - tell me if any of these aren't what was
/// meant, this is an interpretation, not a spec:
/// - ОБНОВЛЕНО 2026-09-18: кровати класса SleepingBag (bed02*, bunkBed*) ТЕПЕРЬ ВКЛЮЧЕНЫ,
/// см. комментарий в TargetPrefixes. НОВЫХ ПАТЧЕЙ ДЛЯ НИХ НЕ ПОНАДОБИЛОСЬ, и это стоило
/// проверить: BlockSleepingBag переопределяет только CanPlaceBlockAt, PlaceBlock,
/// GetActivationText и ЧЕТЫРЁХаргументный OnBlockActivated. Наши три патча висят на
/// HasBlockActivationCommands, GetBlockActivationCommands и ПЯТИаргументном
/// OnBlockActivated - ни одного из них он не перекрывает, поэтому патчи базового Block
/// достают его как есть. С BlockCompositeTileEntity (кулеры, коробки) так не вышло -
/// там понадобился отдельный набор, см. ниже.
///
/// - Исходная формулировка, оставлена для истории:
/// "Кровати" (beds) -> bedMadeNoFrame*/bedMessyNoFrame* ONLY. Deliberately excludes
/// bed02*/bunkBedMade*/bunkBedMessy* even though they look like beds too - decompiling
/// showed those all use Class="SleepingBag" (they're actually functional sleeping-bag/
/// respawn-anchor blocks, not pure decoration - same family as the player's own bedroll,
/// which the user explicitly said NOT to touch). "NoFrame" variants have no Class=
/// override at all (plain decorative furniture), a clean match for "decorative bed".
/// - "Колья" (stakes) -> NOT IMPLEMENTED. The real spike-trap blocks
/// (trapSpikesWood*/trapSpikesIron*) use Class="TrunkTip" (BlockTrunkTip : BlockDamage),
/// which does NOT override GetBlockActivationCommands/OnBlockActivated at all - it isn't
/// built on the activation-command system this "take" mechanic depends on (harvest-node-
/// style blocks are typically hit-to-harvest instead). Making these pickable would need a
/// genuinely different mechanism, not a variant of this one - left out rather than forced
/// in broken. Say if a different "колья" block was meant.
/// - "Кулеры с водой" (water coolers) -> cntWaterCooler* (Class="CompositeTileEntity").
/// - "Коробки" (boxes) -> cntCardboardBox (Class="CompositeTileEntity") - the one
/// unambiguous plain-cardboard-box block; there are dozens of OTHER "*box*" blocks in
/// vanilla (mailboxes, breaker boxes, truck cargo) not included here since they don't
/// read as "coробки, расставленные на карте" the way a cardboard box does.
///
/// TakeDelay (8s) is a guess, not specified by the user - shorter than the workstation
/// default (15s) since these are simpler props, not a full crafting station.
/// NOT VERIFIED IN-GAME - same caution as everything else added 2026-08-29.
/// </summary>
public static class BlockPickupPatch
{
public const float TakeDelay = 8f;
public static readonly string[] TargetPrefixes = new string[]
{
"bedMadeNoFrame",
"bedMessyNoFrame",
// ДОБАВЛЕНО 2026-09-18, И ЭТО ИСПРАВЛЕНИЕ ПРОМАХА ПО ЦЕЛИ. Изначально эти кровати были
// исключены НАМЕРЕННО, с формулировкой "это respawn-anchor блоки, не декорация,
// трогать не просил". Оказалось ровно наоборот: пользователь объяснил, что подбор
// кроватей нужен ИМЕННО ради возрождения - "поглощённые блоки сохраняют состояние,
// которое было до поглощения; кровати до поглощения не являются точкой возрождения;
// точкой возрождения они могли бы стать при подборе".
//
// Проверено декомпиляцией BlockSleepingBag: PlaceBlock ставит игроку
// selectedSpawnPointKey и запоминает BedrollPos - то есть поставленная своими руками
// кровать действительно становится точкой возрождения, а стоящая в POI - нет.
//
// Один префикс покрывает все цветовые варианты: bed02White, bunkBedMessyBlue и прочие
// наследуют Class="SleepingBag" через Extends от bed02 / bunkBedMade.
// Вместе с bunkBed* попадают bunkBedFrame и bunkBedMattress - у них класса нет, они
// просто декор, и подбирать их не вредно.
//
// bedroll НЕ включён намеренно: это собственный предмет игрока, он и так ставится и
// снимается штатно, а bedrollPOI - отдельный декоративный блок, который от bedroll не
// наследуется и точкой возрождения не является.
"bed02",
"bunkBed",
"cntWaterCooler",
"cntCardboardBox",
};
public static bool IsTargetBlock(BlockValue _blockValue)
{
Block block = _blockValue.Block;
if (block == null)
{
return false;
}
string name = block.GetBlockName();
if (string.IsNullOrEmpty(name))
{
return false;
}
foreach (string prefix in TargetPrefixes)
{
if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public static void AppendTakeCommand(BlockValue _blockValue, ref BlockActivationCommand[] __result)
{
if (!IsTargetBlock(_blockValue))
{
return;
}
List<BlockActivationCommand> commands = new List<BlockActivationCommand>(__result ?? Array.Empty<BlockActivationCommand>());
commands.Add(new BlockActivationCommand("take", "hand", true));
__result = commands.ToArray();
}
public static bool HandleTakeActivation(string _commandName, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player, ref bool __result)
{
if (_commandName != "take" || !IsTargetBlock(_blockValue))
{
return true;
}
Debug.Log("[NecromancerTome] BlockPickupPatch: take activated on " + _blockValue.Block.GetBlockName() + " at " + _blockPos);
// Deliberately calls the static TakeItemWithTimer directly (no canTakeCallback -
// null means "always takeable", same default as the base Block.takeItemWithTimer
// virtual's own unconditional `return true`) rather than going through
// BlockWorkstation's land-claim-gated instance wrapper.
Block.TakeItemWithTimer(_blockPos, _blockValue, _player, TakeDelay);
__result = true;
return false;
}
// --- Plain Block-class targets (bedMadeNoFrame*/bedMessyNoFrame*) ---
[HarmonyPatch(typeof(Block), "HasBlockActivationCommands")]
public static class Patch_Block_HasBlockActivationCommands
{
public static void Postfix(BlockValue _blockValue, ref bool __result)
{
if (IsTargetBlock(_blockValue))
{
__result = true;
}
}
}
[HarmonyPatch(typeof(Block), "GetBlockActivationCommands")]
public static class Patch_Block_GetBlockActivationCommands
{
public static void Postfix(BlockValue _blockValue, ref BlockActivationCommand[] __result)
{
AppendTakeCommand(_blockValue, ref __result);
}
}
[HarmonyPatch(typeof(Block), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })]
public static class Patch_Block_OnBlockActivated
{
public static bool Prefix(string _commandName, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player, ref bool __result)
{
return HandleTakeActivation(_commandName, _blockPos, _blockValue, _player, ref __result);
}
}
// --- BlockCompositeTileEntity targets (cntWaterCooler*/cntCardboardBox) - a DIFFERENT
// C# class that overrides the same three methods, so needs its own separate patches;
// see the class-level comment above for why patching Block alone wouldn't reach these. ---
[HarmonyPatch(typeof(BlockCompositeTileEntity), "HasBlockActivationCommands")]
public static class Patch_Composite_HasBlockActivationCommands
{
public static void Postfix(BlockValue _blockValue, ref bool __result)
{
if (IsTargetBlock(_blockValue))
{
__result = true;
}
}
}
[HarmonyPatch(typeof(BlockCompositeTileEntity), "GetBlockActivationCommands")]
public static class Patch_Composite_GetBlockActivationCommands
{
public static void Postfix(BlockValue _blockValue, ref BlockActivationCommand[] __result)
{
AppendTakeCommand(_blockValue, ref __result);
}
}
[HarmonyPatch(typeof(BlockCompositeTileEntity), "OnBlockActivated", new Type[] { typeof(string), typeof(WorldBase), typeof(Vector3i), typeof(BlockValue), typeof(EntityPlayerLocal) })]
public static class Patch_Composite_OnBlockActivated
{
public static bool Prefix(string _commandName, Vector3i _blockPos, BlockValue _blockValue, EntityPlayerLocal _player, ref bool __result)
{
return HandleTakeActivation(_commandName, _blockPos, _blockValue, _player, ref __result);
}
}
}
}