Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида духов и сюжетный финал через Чёрный портал. Локализация на 13 языках. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
197 lines
9.2 KiB
C#
197 lines
9.2 KiB
C#
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(...) && 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);
|
||
}
|
||
}
|
||
}
|
||
}
|