using System.Diagnostics; using ADOFAIRenderer.Core; using TMPro; using UnityEngine; using UnityEngine.UI; namespace ADOFAIRenderer.UI; /// /// Hold-Escape-to-cancel handler shown during a render. /// /// Created by RenderManager when the render reaches the OnScene stage, on a GameObject kept across scene loads. /// While Escape is held, a centered panel shows a countdown from 3 seconds; holding it for the full 3 seconds /// (measured in real time with a , independent of the patched game time) calls /// RenderManager.CancelRender. Releasing Escape earlier hides the panel and resets the countdown. /// The component destroys its GameObject as soon as RenderManager.IsActive is false (stage below OnScene). /// Runs on the main Unity thread. /// /// internal class EscapeHandlerUI : MonoBehaviour { /// Centered countdown panel (background and text); inactive while Escape is not held. private GameObject _containerObj = null!; /// Countdown text inside . private TextMeshProUGUI _textComponent = null!; /// Real-time hold duration of the current Escape press; null while Escape is not held. private Stopwatch? _escapeStopwatch; /// /// Builds the overlay canvas and the hidden countdown panel. Layout values are canvas units at the 1920x1080 /// reference resolution. /// private void Start() { // Set UI layer (5, Unity's built-in "UI" layer) gameObject.layer = 5; // Canvas setup var canvas = gameObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 10000; // Placed above debug monitor // Canvas Scaler: 1920x1080 reference resolution, width and height matched equally (0.5) var scaler = gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = 0.5f; gameObject.AddComponent(); // 1. Create container and background panel object: 460x160 units at the screen center _containerObj = new GameObject("EscapeContainer"); _containerObj.transform.SetParent(transform, false); _containerObj.layer = 5; var bgImage = _containerObj.AddComponent(); bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f); // Dark tone acrylic style var containerRect = _containerObj.GetComponent(); containerRect.anchorMin = new Vector2(0.5f, 0.5f); containerRect.anchorMax = new Vector2(0.5f, 0.5f); containerRect.pivot = new Vector2(0.5f, 0.5f); containerRect.anchoredPosition = Vector2.zero; // Screen center containerRect.sizeDelta = new Vector2(460, 160); // 2. Create text object: 60 pt centered rich text, no wrapping, 10-unit margin inside the panel var textObj = new GameObject("EscapeText"); textObj.transform.SetParent(_containerObj.transform, false); textObj.layer = 5; _textComponent = textObj.AddComponent(); _textComponent.alignment = TextAlignmentOptions.Center; _textComponent.fontSize = 60; _textComponent.textWrappingMode = TextWrappingModes.NoWrap; _textComponent.richText = true; // Apply the game's TextMeshPro font for the current language (RDString.fontData.fontTMP, assigned in // RDString.Setup). It is null before RDString.Setup has run; TextMeshPro's default font is kept then. if (RDString.fontData.fontTMP != null) _textComponent.font = RDString.fontData.fontTMP; var textRect = textObj.GetComponent(); textRect.anchorMin = Vector2.zero; textRect.anchorMax = Vector2.one; textRect.offsetMin = new Vector2(10, 10); textRect.offsetMax = new Vector2(-10, -10); // Initial state disabled _containerObj.SetActive(false); } /// /// Tracks the Escape key every frame: shows and updates the countdown while it is held, cancels the render after /// 3 seconds of holding, and hides the panel when it is released. /// private void Update() { // Destroy self once the render is inactive (stage below OnScene: finished, cancelled or failed) if (!RenderManager.IsActive) { Destroy(gameObject); return; } if (Input.GetKey(KeyCode.Escape)) { // First frame of the press: start timing and show the panel if (_escapeStopwatch == null) { _escapeStopwatch = new Stopwatch(); _escapeStopwatch.Start(); _containerObj.SetActive(true); } var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds; var remaining = Mathf.Max(0f, 3f - elapsed); // Update remaining time text (seconds, one decimal). The text is fixed English, not localized. _textComponent.text = $"Cancelling in {remaining:F1}s..\nHold Esc to cancel render"; // Grow animation: the panel scales linearly from 1.0 to 1.15 over the 3-second hold var scale = 1.0f + elapsed / 3.0f * 0.15f; _containerObj.transform.localScale = new Vector3(scale, scale, 1f); // Held for the full 3 seconds: reset the panel, then cancel if (elapsed < 3f) return; _escapeStopwatch.Stop(); _escapeStopwatch = null; _containerObj.transform.localScale = Vector3.one; // Reset scale _containerObj.SetActive(false); // Cancel rendering: RenderManager.CancelRender resets the render state, stops the encoder as cancelled and // ends the render. StopFFmpeg also destroys this handler's GameObject. if (RenderManager.Instance) RenderManager.Instance.CancelRender(); } else { // Escape released before 3 seconds: stop timing and hide the panel if (_escapeStopwatch == null) return; _escapeStopwatch.Stop(); _escapeStopwatch = null; _containerObj.transform.localScale = Vector3.one; // Reset scale _containerObj.SetActive(false); } } /// Stops the hold stopwatch if a press is in progress. private void OnDestroy() { if (_escapeStopwatch == null) return; _escapeStopwatch.Stop(); _escapeStopwatch = null; } }