using System; using System.Collections.Generic; using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// 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(...) && 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. /// 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 commands = new List(__result ?? Array.Empty()); 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); } } } }