Книга некроманта 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
+99
View File
@@ -0,0 +1,99 @@
using System;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// Duke's note ("Записка от Дюка", item noteDuke01) - user request 2026-08-30: "в момент
/// открытия записки, ставить игру на паузу и проигрывать флэшбек" (at the moment the note is
/// opened, pause the game and play a flashback). Reuses the exact pause+video pipeline
/// already built and tested for the Black Portal Stone (see PortalStonePatch.cs's
/// ActivateBlackPortal - GameManager.Instance.Pause/XUiC_VideoPlayer.PlayVideo, both APIs
/// decompiled there already, same reasoning applies unchanged here).
///
/// FINDING THE RIGHT PATCH POINT: noteDuke01 has no custom C# class of its own - it's a
/// plain Class="Eat" item (items.xml) whose entire "reading" experience is a vanilla trick:
/// PromptTitle="noteDuke01"/PromptDescription="noteDuke01Desc" make the ENGINE ITSELF show
/// the note as a XUiC_MessageBoxWindowGroup.ShowOkCancel(...) confirm box - decompiled
/// ItemActionEat directly and confirmed it has no UI-showing code of its own at all
/// (NeedPrompt/PromptTitle/PromptDescription/bPromptChecked are all read, never acted on,
/// inside that class); the actual ShowOkCancel call lives in the CALLERS instead - two
/// separate, decompiled call sites:
/// 1. ItemClass.ExecuteAction(int, ItemInventoryData, bool, PlayerActionsLocal) - the
/// holding-the-item-and-clicking path.
/// 2. XUiC_ItemStack's inventory "Use" (double-click / context-menu) path.
/// Both funnel through the exact same static XUiC_MessageBoxWindowGroup.ShowOkCancel call -
/// patching THAT one method, instead of either call site separately, covers both input paths
/// with a single patch.
///
/// IDENTIFYING OUR NOTE: ShowOkCancel receives only already-localized strings, not an item
/// reference - decompilation confirms this overload has no ItemValue/ItemClass parameter at
/// all. Matched by comparing the incoming title against Localization.Get("noteDuke01") (the
/// exact PromptTitle key from items.xml) - unique to this one item in the whole game, not a
/// generic vanilla dialog string, so this is a safe match, not a guess.
///
/// FLOW: on match, suppress the real dialog for now (Prefix returns false), pause the game,
/// and play the flashback; only once the video finishes (or is skipped/errors - PlayVideo's
/// own onFinished callback fires in every case, confirmed by decompiling
/// XUiC_VideoPlayer.OnClose/FinishAndClose, so this can never soft-lock the pause) does it
/// unpause and open the REAL note-text dialog (a re-entrant call to ShowOkCancel itself, via
/// a bypass flag so the Prefix doesn't intercept its own follow-up call) - "open note ->
/// flashback -> read text -> confirm", rather than overlapping the video with the text box.
///
/// VIDEO FILE: Video/DukeNoteFlashback.mp4 - the user's real flashback clip (delivered
/// 2026-08-30 as exch/flashbback.mp4), kept as .mp4 rather than renamed to .webm like the
/// Black Portal placeholder: Unity's VideoPlayer component (confirmed by decompiling
/// XUiV_Video - it wraps a plain UnityEngine.Video.VideoPlayer) natively decodes MP4/H.264 on
/// Windows via Media Foundation, and re-labeling an actual MP4 container as .webm would just
/// make it fail to decode (VP8/VP9 container expected, not H.264) - not decompiled/proven
/// that MP4 plays correctly in THIS build, but there is no reason implied by the decompiled
/// code to expect otherwise, and even a decode failure only degrades to a skipped video (see
/// FLOW above), never a stuck pause. **Not confirmed in game.**
/// </summary>
[HarmonyPatch(typeof(XUiC_MessageBoxWindowGroup), "ShowOkCancel")]
public static class Patch_XUiC_MessageBoxWindowGroup_ShowOkCancel_NoteFlashback
{
public const string NoteFlashbackVideoPath = "@modfolder(NecromancerTome):Video/DukeNoteFlashback.mp4";
/// <summary>Guards the re-entrant call this patch makes to the very method it patches
/// (to actually show the note text once the flashback is done) - without this, that
/// second call would just trigger the Prefix again and loop back into another flashback
/// instead of showing the dialog. Not [ThreadStatic]: XUi/UI code in this game only ever
/// runs on the main thread (every other UI-touching patch in this mod makes the same
/// assumption, e.g. PortalStonePatch.cs's local-player-only UI calls), so a plain static
/// bool is enough here.</summary>
public static bool bypass;
public static bool Prefix(XUi _xuiInstance, string _title, string _text, string _icon, Action _onOk, Action _onCancel, bool _openMainMenuOnClose, bool _modal, bool _cancelOnOutsideClick)
{
if (bypass)
{
return true;
}
if (_xuiInstance == null || _title != Localization.Get("noteDuke01"))
{
return true;
}
Debug.Log("[NecromancerTome] NoteFlashbackPatch: Duke's note opened, pausing + playing flashback");
GameManager.Instance.Pause(true);
VideoData videoData = new VideoData { url = NoteFlashbackVideoPath };
XUiC_VideoPlayer.PlayVideo(_xuiInstance, videoData, true, delegate(bool skipped)
{
Debug.Log("[NecromancerTome] NoteFlashbackPatch: flashback finished (skipped=" + skipped + "), unpausing and showing note text");
GameManager.Instance.Pause(false);
bypass = true;
try
{
XUiC_MessageBoxWindowGroup.ShowOkCancel(_xuiInstance, _title, _text, _icon, _onOk, _onCancel, _openMainMenuOnClose, _modal, _cancelOnOutsideClick);
}
finally
{
bypass = false;
}
});
return false;
}
}
}