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