Книга некроманта 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
This commit is contained in:
Alex Cube
2026-09-09 21:13:03 +03:00
co-authored by Claude Opus 5
commit e8f064f5ec
102 changed files with 7010 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
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);
}
}
}
}