Files
necromants-tome-7d2d-3-2/HarmonySrc/NoteFlashbackPatch.cs
T
AlexCubeandClaude Opus 5 026e006c9c Убран мёртвый код проигрывания видео; заготовка своей модели ножа
PlayBlackPortalVideoLegacy и BlackPortalVideoPath ниоткуда не вызывались
с 2026-09-09, когда концовку заменили слайдами. Разборы Pause/PlayVideo и
синтаксиса @modfolder перенесены в NoteFlashbackPatch - единственное место,
где эти API ещё вызываются. Перекрёстные ссылки в FinalSlides поправлены,
устаревшее упоминание StayVideoPath/ReturnVideoPath убрано.

В items.xml у necroWpnBladeNecroKnife добавлена закомментированная строка
Meshfile под будущий бандл Resources/necroknife - включать вместе с самим
бандлом, не раньше. Там же снято старое сомнение про TintColor: ваниль сама
тинтит тот же boneShivPrefab, значит механизм на оружии работает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3pwyLpTrwz41qBjyjzNSA
2026-09-10 02:07:31 +03:00

117 lines
6.9 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). Built on the pause+video pipeline first
/// written for the Black Portal Stone; since the finale switched to text slides
/// (FinalSlides) and its dead video code was removed 2026-09-10, this patch is the only
/// place in the mod that still calls either API, so both write-ups live here now:
/// - GameManager.Instance.Pause(bool) - decompiled GameManager.updatePauseState: sets
/// Time.timeScale=0 for real, but ONLY takes effect in singleplayer (an SP-only check
/// baked into vanilla itself, not a limitation added by this mod) - a deliberate,
/// documented no-op in multiplayer rather than something silently broken.
/// - XUiC_VideoPlayer.PlayVideo(xui, VideoData, skippable, onFinished) - opens the same
/// fullscreen "VideoPlayer" window vanilla's own TFP intro/menu-background videos use.
/// Decompiled XUiV_Video confirms video playback isn't gated by Time.timeScale, so it
/// keeps playing correctly while paused. skippable=true (Cancel key) so a broken/
/// missing video file can't soft-lock the player - XUiV_Video.OnVideoErrorReceived
/// already auto-closes on a bad file on its own, this is just a second, player-facing
/// way out.
///
/// 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), and the only video the mod still ships. Kept as
/// .mp4 rather than renamed to .webm like the since-deleted 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
{
/// <summary>"@modfolder(NecromancerTome):..." is the exact mod-relative path syntax
/// XUiV_Video.startVideo resolves via ModManager.TryPatchModPathString (decompiled to
/// confirm - looks for "@modfolder(&lt;mod name&gt;):" and substitutes the mod's real
/// install path; "NecromancerTome" here is this mod's own ModInfo.xml Name, not its
/// DisplayName).</summary>
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;
}
}
}