Files
necromants-tome-7d2d-3-2/HarmonySrc/BlockPickupPatch.cs
T
Alex CubeandClaude Opus 5 e8f064f5ec Книга некроманта 1.0 — первая публичная версия
Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых
зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида
духов и сюжетный финал через Чёрный портал. Локализация на 13 языках.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:13:03 +03:00

197 lines
9.2 KiB
C#
Raw 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:
/// - "Кровати" (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",
"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);
}
}
}
}