SHA256
Document UI And Localization
This commit is contained in:
1 parent
a389283d33
commit
0fc01a84de
8 files changed
+908
-69
No files matched your search
@@ -9,29 +9,49 @@ namespace ADOFAIRenderer.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to localized strings and handles language synchronization with the game.
|
||||
/// <para>
|
||||
/// Locale files are flat JSON objects (<c>"key": "text"</c>) under <c>Localization/Locale/</c>, embedded in the mod
|
||||
/// assembly as resources; the file name without extension is the locale code (<c>en.json</c> -> "en").
|
||||
/// The active locale follows the game language: <c>LanguageSyncPatch</c> calls <see cref="SetLanguage"/> after the game's
|
||||
/// <c>RDString.Setup</c> and <c>RDString.ChangeLanguage</c>. Only Korean maps to its own locale ("ko"); every other
|
||||
/// language uses English ("en").
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Lookup order in <see cref="Get(string)"/>: active locale, then English, then the key itself.
|
||||
/// Intended for the Unity main thread; the dictionaries are filled once during initialization and only read afterwards.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class LocalizationManager
|
||||
{
|
||||
/// <summary>Loaded locales: locale code (case-insensitive) -> key -> text. Keys are case-sensitive.</summary>
|
||||
private static readonly Dictionary<string, Dictionary<string, string>> Locales = new(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>Locale used when the language has no locale file and for keys missing in the active locale.</summary>
|
||||
private static readonly string FallbackLocaleCode = "en";
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when the active language changes.
|
||||
/// Raised by <see cref="SetLanguage"/> with <c>notify: true</c> when the language or the resolved locale differs from
|
||||
/// the current one; the argument is the new game language. Exceptions from handlers are caught and logged
|
||||
/// (an exception stops the remaining handlers of that invocation).
|
||||
/// </summary>
|
||||
public static event Action<SystemLanguage>? OnLanguageChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Currently active SystemLanguage.
|
||||
/// Currently active SystemLanguage: the game language as passed to <see cref="SetLanguage"/>, also when it has no
|
||||
/// locale file of its own (e.g. Japanese, which displays English text).
|
||||
/// </summary>
|
||||
public static SystemLanguage CurrentLanguage { get; private set; } = SystemLanguage.English;
|
||||
|
||||
/// <summary>
|
||||
/// Currently active locale code (e.g. "en", "ko").
|
||||
/// Currently active locale code (e.g. "en", "ko"): the locale whose strings <see cref="Get(string)"/> returns first.
|
||||
/// Always a loaded locale or the fallback "en".
|
||||
/// </summary>
|
||||
public static string CurrentLocaleCode { get; private set; } = "en";
|
||||
|
||||
/// <summary>Set by the first <see cref="Initialize"/> call; later calls return immediately.</summary>
|
||||
private static bool _initialized;
|
||||
|
||||
/// <summary>Initializes on first access to any member, so lookups work before <c>RenderMod</c> calls <see cref="Initialize"/>.</summary>
|
||||
static LocalizationManager()
|
||||
{
|
||||
Initialize();
|
||||
@@ -39,6 +59,8 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the localization system and loads embedded language dictionaries.
|
||||
/// Runs once; further calls do nothing. Selects the initial language without raising
|
||||
/// <see cref="OnLanguageChanged"/>.
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
@@ -47,19 +69,26 @@ internal static class LocalizationManager
|
||||
|
||||
LoadEmbeddedLocales();
|
||||
|
||||
// Detect initial language from game settings
|
||||
// Detect initial language from game settings.
|
||||
// At mod initialization RDString.Setup has usually not run yet, so this reads the field's initial value
|
||||
// (English); LanguageSyncPatch applies the real language when the game calls RDString.Setup.
|
||||
var initialLang = DetectGameLanguage();
|
||||
SetLanguage(initialLang, notify: false);
|
||||
|
||||
LogMsg($"[Localization] Initialized. Active language: {CurrentLanguage} ({CurrentLocaleCode})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs through the MelonLoader logger of the mod, or to the console when that logger is not available
|
||||
/// (e.g. when this class is used outside the running mod).
|
||||
/// </summary>
|
||||
private static void LogMsg(string msg)
|
||||
{
|
||||
try { Melon<RenderMod>.Logger.Msg(msg); }
|
||||
catch { Console.WriteLine(msg); }
|
||||
}
|
||||
|
||||
/// <summary>Error counterpart of <see cref="LogMsg"/>; falls back to standard error.</summary>
|
||||
private static void LogError(string msg)
|
||||
{
|
||||
try { Melon<RenderMod>.Logger.Error(msg); }
|
||||
@@ -68,6 +97,9 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Loads all *.json locale files embedded in this assembly.
|
||||
/// A resource is used when its name ends with ".json" and contains "Localization.Locale" (the manifest name built
|
||||
/// from the folder path, see the EmbeddedResource item in the .csproj). A file that fails to read or parse is
|
||||
/// logged and skipped; a later resource with the same locale code replaces an earlier one.
|
||||
/// </summary>
|
||||
private static void LoadEmbeddedLocales()
|
||||
{
|
||||
@@ -109,7 +141,13 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Sets the active language and notifies subscribers if changed.
|
||||
/// The locale is chosen by <see cref="LanguageToLocaleCode"/>; when that locale is not loaded, "en" is used.
|
||||
/// </summary>
|
||||
/// <param name="language">Game language (as used by <c>RDString.language</c>).</param>
|
||||
/// <param name="notify">
|
||||
/// Whether to raise <see cref="OnLanguageChanged"/> when the language or locale changes; <c>false</c> is used for
|
||||
/// the initial selection.
|
||||
/// </param>
|
||||
public static void SetLanguage(SystemLanguage language, bool notify = true)
|
||||
{
|
||||
var localeCode = LanguageToLocaleCode(language);
|
||||
@@ -140,7 +178,10 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a localized string for the specified key.
|
||||
/// Order: active locale, then English, then the key itself (so a missing key is visible in the UI).
|
||||
/// </summary>
|
||||
/// <param name="key">Localization key, e.g. <c>"ui.render_config.window_title"</c>; case-sensitive.</param>
|
||||
/// <returns>The localized text, the key when no locale has it, or an empty string for a null/empty key.</returns>
|
||||
public static string Get(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return string.Empty;
|
||||
@@ -166,7 +207,12 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a formatted localized string.
|
||||
/// The text from <see cref="Get(string)"/> is used as a <see cref="string.Format(string, object[])"/> template
|
||||
/// (<c>{0}</c>, <c>{1}</c>, ...), formatted with the current culture. When the template does not match the
|
||||
/// arguments (<see cref="FormatException"/>), the unformatted template is returned.
|
||||
/// </summary>
|
||||
/// <param name="key">Localization key.</param>
|
||||
/// <param name="args">Format arguments; with none, the template is returned as is.</param>
|
||||
public static string Get(string key, params object[] args)
|
||||
{
|
||||
var template = Get(key);
|
||||
@@ -184,6 +230,7 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Maps Unity's SystemLanguage enum to an internal locale code.
|
||||
/// Korean -> "ko"; every other language -> "en". A new locale file also needs an entry here.
|
||||
/// </summary>
|
||||
private static string LanguageToLocaleCode(SystemLanguage language)
|
||||
{
|
||||
@@ -196,19 +243,22 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to read the game's current language from RDString or Persistence.
|
||||
/// Falls back to English when both reads throw.
|
||||
/// </summary>
|
||||
private static SystemLanguage DetectGameLanguage()
|
||||
{
|
||||
try
|
||||
{
|
||||
// First attempt: RDString.language
|
||||
// First attempt: RDString.language, a public static field of the game (initial value English,
|
||||
// assigned from Persistence.language in RDString.Setup)
|
||||
return RDString.language;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fallback attempt: Persistence.language
|
||||
// Fallback attempt: Persistence.language, the saved "language" preference (defaults to
|
||||
// Application.systemLanguage)
|
||||
return Persistence.language;
|
||||
}
|
||||
catch
|
||||
@@ -221,6 +271,7 @@ internal static class LocalizationManager
|
||||
|
||||
/// <summary>
|
||||
/// Convenient shorthand alias for <see cref="LocalizationManager"/>.
|
||||
/// Every member forwards to the member of the same name in <see cref="LocalizationManager"/>.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal static class I18n
|
||||
|
||||
@@ -11,14 +11,29 @@ using Image = UnityEngine.UI.Image;
|
||||
|
||||
namespace ADOFAIRenderer.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Description of one button of a <see cref="CommonWarningDialogUI"/> dialog: label, click action and optional color.
|
||||
/// </summary>
|
||||
internal class DialogButton
|
||||
{
|
||||
// ReSharper disable FieldCanBeMadeReadOnly.Global
|
||||
/// <summary>Button label, shown as given (callers pass already localized text).</summary>
|
||||
public string Text;
|
||||
|
||||
/// <summary>
|
||||
/// Action run on the Unity main thread when the button is clicked. The dialog is destroyed after the action
|
||||
/// returns, also when the action throws (the exception is logged).
|
||||
/// </summary>
|
||||
public Action OnClick;
|
||||
|
||||
/// <summary>Normal (idle) background color of the button; <c>null</c> keeps the O5Kit theme button color.</summary>
|
||||
public Color? BackgroundColor;
|
||||
// ReSharper restore FieldCanBeMadeReadOnly.Global
|
||||
|
||||
/// <summary>Creates a button description.</summary>
|
||||
/// <param name="text">Button label.</param>
|
||||
/// <param name="onClick">Action run on click, before the dialog closes.</param>
|
||||
/// <param name="backgroundColor">Optional normal background color; <c>null</c> uses the theme color.</param>
|
||||
public DialogButton(string text, Action onClick, Color? backgroundColor = null)
|
||||
{
|
||||
Text = text;
|
||||
@@ -27,10 +42,29 @@ internal class DialogButton
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modal message dialog with a title, a description and one or more buttons.
|
||||
/// <para>
|
||||
/// Each <see cref="ShowDialog"/> call creates its own GameObject (kept across scene loads) with a screen-space overlay
|
||||
/// canvas: a translucent full-screen backdrop that blocks input to everything below, and an O5Kit window on top of it.
|
||||
/// While the dialog exists, every enabled UI Toolkit root (<see cref="UIDocument"/>) is disabled so that it cannot
|
||||
/// receive input; the roots are enabled again in <see cref="OnDestroy"/>. Clicking any button runs its action and then
|
||||
/// destroys the dialog. All members run on the Unity main thread.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class CommonWarningDialogUI : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The most recently created dialog, or <c>null</c> when that dialog has been destroyed.
|
||||
/// <c>FirstUpdateHook</c> checks it to wait until a startup dialog is closed.
|
||||
/// If several dialogs are open at once, this refers to the newest one only.
|
||||
/// </summary>
|
||||
public static CommonWarningDialogUI? Instance { get; private set; }
|
||||
|
||||
/// <summary>UI Toolkit root elements disabled by this dialog, enabled again when the dialog is destroyed.</summary>
|
||||
private readonly List<VisualElement> _disabledElements = new();
|
||||
|
||||
/// <summary>O5Kit window holding the title, description and buttons; disposed in <see cref="OnDestroy"/>.</summary>
|
||||
private O5Window? _window;
|
||||
|
||||
private void Awake()
|
||||
@@ -38,6 +72,10 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the window, enables the UI Toolkit roots disabled in <see cref="Initialize"/> and clears
|
||||
/// <see cref="Instance"/> if it still refers to this dialog.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
_window?.Dispose();
|
||||
@@ -58,6 +96,15 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and shows a new dialog. Must be called on the Unity main thread.
|
||||
/// </summary>
|
||||
/// <param name="title">Window title text.</param>
|
||||
/// <param name="description">Body text, centered and word-wrapped; TextMeshPro rich text tags are allowed.</param>
|
||||
/// <param name="buttons">
|
||||
/// Buttons laid out left to right with equal widths. With no buttons nothing is shown, because the dialog
|
||||
/// has no close button and could not be dismissed.
|
||||
/// </param>
|
||||
public static void ShowDialog(string title, string description, params DialogButton[] buttons)
|
||||
{
|
||||
if (buttons.Length == 0) return;
|
||||
@@ -68,8 +115,14 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
dialog.Initialize(title, description, buttons);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the canvas, backdrop, window and contents. Layout sizes are in canvas units at the 1920x1080 reference
|
||||
/// resolution of the <see cref="CanvasScaler"/>.
|
||||
/// </summary>
|
||||
private void Initialize(string title, string description, DialogButton[] buttons)
|
||||
{
|
||||
// uGUI buttons need an EventSystem to receive clicks. When the current scene has none, a child EventSystem
|
||||
// is created; it is destroyed together with the dialog.
|
||||
if (!UnityEngine.EventSystems.EventSystem.current)
|
||||
{
|
||||
var eventSystemObj = new GameObject("EventSystem");
|
||||
@@ -78,6 +131,8 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
eventSystemObj.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
|
||||
}
|
||||
|
||||
// Disable every enabled UI Toolkit root so that it ignores input while the dialog is open.
|
||||
// Roots that are already disabled are left alone and are not re-enabled in OnDestroy.
|
||||
foreach (var doc in FindObjectsByType<UIDocument>(FindObjectsSortMode.None))
|
||||
{
|
||||
if (doc && doc.rootVisualElement is { enabledSelf: true })
|
||||
@@ -87,14 +142,17 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Set UI layer (5)
|
||||
// Set UI layer (5, Unity's built-in "UI" layer)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas setup
|
||||
// Canvas setup.
|
||||
// Sorting order 32766 draws the dialog above the RendererUIManager window canvas (32000), the render overlays
|
||||
// (DebugMonitorUI 9999, EscapeHandlerUI 10000) and below FFmpegDownloadUI (32767, the largest sorting order).
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 32766;
|
||||
|
||||
// Scale with screen size against a 1920x1080 reference; match 0.5 blends width and height scaling equally.
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -105,7 +163,8 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
|
||||
var ctx = RendererUIManager.Context;
|
||||
|
||||
// 1. Create full screen backdrop (scrim) to block background input
|
||||
// 1. Create full screen backdrop (scrim) to block background input.
|
||||
// Black at 65% opacity, stretched over the whole canvas; raycastTarget makes it swallow clicks.
|
||||
var bgPanelObj = new GameObject("Backdrop");
|
||||
bgPanelObj.transform.SetParent(transform, false);
|
||||
bgPanelObj.layer = 5;
|
||||
@@ -120,7 +179,8 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
bgRect.offsetMin = Vector2.zero;
|
||||
bgRect.offsetMax = Vector2.zero;
|
||||
|
||||
// 2. Create actual native O5Window
|
||||
// 2. Create the O5Kit window as a child of the backdrop: 540x240 units, draggable, fixed size,
|
||||
// no close button (the dialog closes only through its buttons), 16-unit inner padding.
|
||||
_window = O5Window.Create(ctx, bgPanelObj.transform, new O5WindowOptions
|
||||
{
|
||||
Id = "RendererMod_WarningDialog",
|
||||
@@ -133,7 +193,8 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
Padding = new RectOffset(16, 16, 16, 16)
|
||||
});
|
||||
|
||||
// 3. Build content inside window.Content
|
||||
// 3. Build content inside window.Content: a vertical stack of the description (takes the remaining height)
|
||||
// and the button row (40 units high), 16 units apart.
|
||||
var content = _window.Content;
|
||||
|
||||
var vLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
@@ -144,7 +205,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
vLayout.childForceExpandWidth = true;
|
||||
vLayout.childForceExpandHeight = false;
|
||||
|
||||
// Description text
|
||||
// Description text: 18 pt, centered, word-wrapped, light gray (88% white)
|
||||
var descText = O5Factory.ControlText(ctx, content, 18f, noPad: true);
|
||||
descText.text = description;
|
||||
descText.alignment = TextAlignmentOptions.Center;
|
||||
@@ -157,7 +218,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
descLE.flexibleWidth = 1f;
|
||||
descLE.flexibleHeight = 1f;
|
||||
|
||||
// Button action row
|
||||
// Button action row: buttons share the row width equally, 10 units apart
|
||||
var btnRowObj = new GameObject("ButtonRow");
|
||||
btnRowObj.transform.SetParent(content, false);
|
||||
var btnRowRect = btnRowObj.AddComponent<RectTransform>();
|
||||
@@ -176,6 +237,7 @@ internal class CommonWarningDialogUI : MonoBehaviour
|
||||
for (var i = 0; i < buttons.Length; i++)
|
||||
{
|
||||
var btnData = buttons[i];
|
||||
// The click handler runs the caller's action, logs any exception from it, and always destroys the dialog.
|
||||
var btn = O5Factory.Button(
|
||||
ctx: ctx,
|
||||
parent: btnRowRect,
|
||||
|
||||
+62
-12
@@ -8,39 +8,73 @@ using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.UI;
|
||||
|
||||
/// <summary>
|
||||
/// On-screen overlay shown while a render is running: a statistics panel in the top-right corner and a level progress
|
||||
/// bar near the bottom of the screen.
|
||||
/// <para>
|
||||
/// Created by <c>RenderManager.StartFFmpegThread</c> on a GameObject kept across scene loads, and reused for later
|
||||
/// renders. The overlay is drawn on the game screen only; it is not part of the encoded video frames.
|
||||
/// <see cref="Update"/> rebuilds the text every frame on the Unity main thread.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class DebugMonitorUI : MonoBehaviour
|
||||
{
|
||||
// --- Progress Bar UI Configuration Constants ---
|
||||
// Sizes and positions are canvas units at the 1920x1080 reference resolution of the CanvasScaler.
|
||||
|
||||
/// <summary>Width of the progress bar; the fill width is <c>BarWidth * progress</c>.</summary>
|
||||
private const float BarWidth = 800f;
|
||||
/// <summary>Height of the progress bar.</summary>
|
||||
private const float BarHeight = 28f;
|
||||
/// <summary>Horizontal offset of the bar center from the screen center (0 = centered).</summary>
|
||||
private const float BarPosX = 0f;
|
||||
/// <summary>Vertical offset of the bar center from the screen center; -420 is 120 units above the bottom edge (540 - 420).</summary>
|
||||
private const float BarPosY = -420f; // Y position relative to screen center (Reference: 1920x1080)
|
||||
/// <summary>Font size of the percentage text drawn on the bar.</summary>
|
||||
private const int BarTextFontSize = 16; // Increased font size for visibility
|
||||
/// <summary>Unfilled bar color: opaque near-black with a slight blue tint.</summary>
|
||||
private static readonly Color BarBackgroundColor = new(0.08f, 0.08f, 0.1f, 1.0f);
|
||||
/// <summary>Filled part color: opaque cyan (#00CCFF).</summary>
|
||||
private static readonly Color BarFillColor = new(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue
|
||||
/// <summary>Percentage text color: opaque white.</summary>
|
||||
private static readonly Color BarTextColor = new(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
// --- Progress Bar Outline Configuration Constants ---
|
||||
|
||||
/// <summary>Color of the uGUI <see cref="Outline"/> drawn around the bar background.</summary>
|
||||
private static readonly Color BarOutlineColor = Color.white; // Contrasting white outline
|
||||
/// <summary>
|
||||
/// <c>Outline.effectDistance</c> of the bar outline: the background is redrawn shifted by +-3 units on both axes,
|
||||
/// which shows as a border about 3 units wide.
|
||||
/// </summary>
|
||||
private static readonly Vector2 BarOutlineDistance = new(3.0f, -3.0f); // Outline thickness
|
||||
|
||||
/// <summary>Statistics text inside the top-right panel.</summary>
|
||||
private TextMeshProUGUI _text = null!;
|
||||
/// <summary>Progress bar background object (parent of the fill and the percentage text).</summary>
|
||||
private GameObject _progressBarObj = null!;
|
||||
/// <summary>Progress bar fill object.</summary>
|
||||
private GameObject _progressBarFillObj = null!;
|
||||
/// <summary>Fill rect, anchored to the left edge of the bar; its width is set every frame.</summary>
|
||||
private RectTransform _fillRect = null!;
|
||||
/// <summary>Text on the bar: percentage and "current floor / last floor index".</summary>
|
||||
private TextMeshProUGUI _progressText = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the overlay canvas, the statistics panel and the progress bar. Layout values are canvas units at the
|
||||
/// 1920x1080 reference resolution.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
// Set UI layer (5)
|
||||
// Set UI layer (5, Unity's built-in "UI" layer)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas setup
|
||||
// Canvas setup. Sorting order 9999 is below EscapeHandlerUI (10000) and the dialogs/windows of the mod.
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 9999;
|
||||
|
||||
// Dynamic resolution scaling setup (Reference: 1920x1080)
|
||||
// Dynamic resolution scaling against a 1920x1080 reference resolution; match 0.5 blends width and height
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -49,7 +83,7 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// Create background panel
|
||||
// Create background panel: 360x220 units, pivot and anchor at the top-right corner, inset 30 units from both edges
|
||||
var panelObj = new GameObject("BackgroundPanel");
|
||||
panelObj.transform.SetParent(transform, false);
|
||||
panelObj.layer = 5;
|
||||
@@ -64,7 +98,7 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
panelRect.anchoredPosition = new Vector2(-30, -30);
|
||||
panelRect.sizeDelta = new Vector2(360, 220);
|
||||
|
||||
// Create text
|
||||
// Create text: 16 pt rich text, top-left aligned, filling the panel minus a 20-unit margin
|
||||
var textObj = new GameObject("MonitorText");
|
||||
textObj.transform.SetParent(panelObj.transform, false);
|
||||
textObj.layer = 5;
|
||||
@@ -74,11 +108,11 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
_text.fontSize = 16;
|
||||
_text.alignment = TextAlignmentOptions.TopLeft;
|
||||
|
||||
// Prevent TMPro text clipping
|
||||
// Wrap long lines and let text that exceeds the panel height overflow instead of being truncated
|
||||
_text.textWrappingMode = TextWrappingModes.Normal;
|
||||
_text.overflowMode = TextOverflowModes.Overflow;
|
||||
|
||||
// Load and apply font
|
||||
// Apply the mod's bundled font (loaded by ModAssets.Load at mod initialization)
|
||||
_text.font = ModAssets.NanumSquareNeoNormal;
|
||||
|
||||
var textRect = textObj.GetComponent<RectTransform>();
|
||||
@@ -88,7 +122,7 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
textRect.offsetMax = new Vector2(-20, -20);
|
||||
|
||||
// --- Progress Bar UI Setup ---
|
||||
// 1. Progress Bar Container
|
||||
// 1. Progress Bar Container: centered anchor/pivot, placed at (BarPosX, BarPosY) from the screen center
|
||||
_progressBarObj = new GameObject("ProgressBar");
|
||||
_progressBarObj.transform.SetParent(transform, false);
|
||||
_progressBarObj.layer = 5;
|
||||
@@ -108,7 +142,8 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
barRect.anchoredPosition = new Vector2(BarPosX, BarPosY);
|
||||
barRect.sizeDelta = new Vector2(BarWidth, BarHeight);
|
||||
|
||||
// 2. Progress Bar Fill
|
||||
// 2. Progress Bar Fill: anchored to the left edge and stretched vertically (anchor x 0..0, y 0..1).
|
||||
// sizeDelta.x is the fill width in units; Update sets it to BarWidth * progress.
|
||||
_progressBarFillObj = new GameObject("ProgressBarFill");
|
||||
_progressBarFillObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
_progressBarFillObj.layer = 5;
|
||||
@@ -123,7 +158,7 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
_fillRect.anchoredPosition = Vector2.zero;
|
||||
_fillRect.sizeDelta = new Vector2(0, 0); // width will be updated dynamically in Update()
|
||||
|
||||
// 3. Progress Bar Text
|
||||
// 3. Progress Bar Text: centered over the whole bar
|
||||
var progressTextObj = new GameObject("ProgressBarText");
|
||||
progressTextObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
progressTextObj.layer = 5;
|
||||
@@ -135,7 +170,7 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
_progressText.font = ModAssets.NanumSquareNeoNormal;
|
||||
_progressText.color = BarTextColor;
|
||||
|
||||
// Add progress bar text outline for readability
|
||||
// Black outline (90% opacity, 1.5-unit offset) keeps the white text readable over both the fill and the background
|
||||
var textOutline = progressTextObj.AddComponent<Outline>();
|
||||
textOutline.effectColor = new Color(0f, 0f, 0f, 0.9f);
|
||||
textOutline.effectDistance = new Vector2(1.5f, -1.5f);
|
||||
@@ -147,10 +182,22 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
textRectBar.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the statistics text and the progress bar from <see cref="RenderManager"/> and the game state.
|
||||
/// Values fall back to 0 (and the queue limit to <c>RenderManager.MaxQueuedFrames</c>) when no RenderManager exists.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!_text) return;
|
||||
|
||||
// Statistics lines:
|
||||
// Queue - frames waiting in ImageQueue for the render thread / queue frame limit
|
||||
// Rendered Frames - RenderManager.frameCount (frames produced by the game)
|
||||
// Frames Sent - RenderManager.framesSent (frames handed to the encoder; written by the render thread)
|
||||
// Throttling - RenderManager.throttleCount (times the main thread started waiting because ImageQueue
|
||||
// held more frames than its limit)
|
||||
// Readback failures- RenderManager.readbackFailureCount (AsyncGPUReadback requests that completed with an error)
|
||||
// Mono heap - GC.GetTotalMemory(false), managed heap size in MiB without forcing a collection
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
|
||||
sb.AppendLine();
|
||||
@@ -158,6 +205,7 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
var queueCount = 0;
|
||||
if (RenderManager.Instance)
|
||||
{
|
||||
// ImageQueue is shared with the render thread, so its count is read under the same lock
|
||||
lock (RenderManager.Instance.ImageQueue)
|
||||
{
|
||||
queueCount = RenderManager.Instance.ImageQueue.Count;
|
||||
@@ -177,7 +225,9 @@ internal class DebugMonitorUI : MonoBehaviour
|
||||
|
||||
_text.text = sb.ToString();
|
||||
|
||||
// Update Progress Bar
|
||||
// Update Progress Bar.
|
||||
// progress = seqID of the current floor / index of the last floor (listFloors.Count - 1), clamped to 0..1.
|
||||
// The text shows the percentage with one decimal and "(current / last)".
|
||||
var progress = 0f;
|
||||
var currentFloor = 0;
|
||||
var totalFloors = 0;
|
||||
|
||||
+37
-9
@@ -6,15 +6,33 @@ using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Hold-Escape-to-cancel handler shown during a render.
|
||||
/// <para>
|
||||
/// Created by <c>RenderManager</c> 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 <see cref="Stopwatch"/>, independent of the patched game time) calls
|
||||
/// <c>RenderManager.CancelRender</c>. Releasing Escape earlier hides the panel and resets the countdown.
|
||||
/// The component destroys its GameObject as soon as <c>RenderManager.IsActive</c> is false (stage below OnScene).
|
||||
/// Runs on the Unity main thread.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class EscapeHandlerUI : MonoBehaviour
|
||||
{
|
||||
/// <summary>Centered countdown panel (background and text); inactive while Escape is not held.</summary>
|
||||
private GameObject _containerObj = null!;
|
||||
/// <summary>Countdown text inside <see cref="_containerObj"/>.</summary>
|
||||
private TextMeshProUGUI _textComponent = null!;
|
||||
/// <summary>Real-time hold duration of the current Escape press; <c>null</c> while Escape is not held.</summary>
|
||||
private Stopwatch? _escapeStopwatch;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the overlay canvas and the hidden countdown panel. Layout values are canvas units at the 1920x1080
|
||||
/// reference resolution.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
// Set UI layer (5)
|
||||
// Set UI layer (5, Unity's built-in "UI" layer)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas setup
|
||||
@@ -22,7 +40,7 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 10000; // Placed above debug monitor
|
||||
|
||||
// Responsive Canvas Scaler setup
|
||||
// Canvas Scaler: 1920x1080 reference resolution, width and height matched equally (0.5)
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -31,7 +49,7 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
|
||||
gameObject.AddComponent<GraphicRaycaster>();
|
||||
|
||||
// 1. Create container and background panel object
|
||||
// 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;
|
||||
@@ -46,7 +64,7 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
containerRect.anchoredPosition = Vector2.zero; // Screen center
|
||||
containerRect.sizeDelta = new Vector2(460, 160);
|
||||
|
||||
// 2. Create text object
|
||||
// 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;
|
||||
@@ -57,7 +75,8 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
_textComponent.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
_textComponent.richText = true;
|
||||
|
||||
// Apply game's default font (RDString.fontData.fontTMP)
|
||||
// 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;
|
||||
|
||||
@@ -71,9 +90,13 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
_containerObj.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
// Destroy self if rendering is no longer active
|
||||
// Destroy self once the render is inactive (stage below OnScene: finished, cancelled or failed)
|
||||
if (!RenderManager.IsActive)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
@@ -82,6 +105,7 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
|
||||
if (Input.GetKey(KeyCode.Escape))
|
||||
{
|
||||
// First frame of the press: start timing and show the panel
|
||||
if (_escapeStopwatch == null)
|
||||
{
|
||||
_escapeStopwatch = new Stopwatch();
|
||||
@@ -92,13 +116,14 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
|
||||
var remaining = Mathf.Max(0f, 3f - elapsed);
|
||||
|
||||
// Update remaining time text
|
||||
// Update remaining time text (seconds, one decimal). The text is fixed English, not localized.
|
||||
_textComponent.text = $"<color=#FF5252>Cancelling in {remaining:F1}s..</color>\n<size=40><color=#DDDDDD>Hold Esc to cancel render</color></size>";
|
||||
|
||||
// Micro pulse animation (container smoothly scales up while holding)
|
||||
// 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();
|
||||
@@ -106,12 +131,14 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
_containerObj.transform.localScale = Vector3.one; // Reset scale
|
||||
_containerObj.SetActive(false);
|
||||
|
||||
// Cancel rendering and exit scene
|
||||
// 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;
|
||||
@@ -120,6 +147,7 @@ internal class EscapeHandlerUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stops the hold stopwatch if a press is in progress.</summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_escapeStopwatch == null) return;
|
||||
|
||||
+142
-17
@@ -15,27 +15,71 @@ using UnityEngine.UI;
|
||||
|
||||
namespace ADOFAIRenderer.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Modal dialog that installs the FFmpeg shared libraries into MelonLoader's UserLibs folder.
|
||||
/// <para>
|
||||
/// Shown by <c>FirstUpdateHook</c> when <see cref="CheckFFmpegExists"/> reports missing libraries. On Windows and Linux
|
||||
/// the "Download" button downloads a prebuilt BtbN FFmpeg build (GPL, shared libraries) and extracts the libraries
|
||||
/// into UserLibs; on macOS the button opens https://ffmpeg.org, because the dialog text tells the user to build FFmpeg
|
||||
/// manually there. The libraries are found by FFmpeg.AutoGen through <c>ffmpeg.RootPath</c> (set to UserLibs in
|
||||
/// <c>RenderMod.OnInitializeMelon</c>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Threads: UI construction and all Unity object access happen on the Unity main thread. The download and extraction
|
||||
/// run on thread-pool threads (<see cref="Task.Run(Func{Task})"/>); they post UI updates to
|
||||
/// <see cref="_mainThreadQueue"/>, which <see cref="Update"/> drains on the main thread.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Flow: buttons -> (Download) progress bar + status text -> download to a temporary archive in UserLibs -> extract
|
||||
/// libraries -> "complete" for 1 second -> dialog destroyed. On failure the status shows the error and the buttons
|
||||
/// return so the user can retry or cancel. Cancel destroys the dialog at once and cancels a running download.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class FFmpegDownloadUI : MonoBehaviour
|
||||
{
|
||||
/// <summary>Row with the Download/Open Website and Cancel buttons; hidden while a download runs.</summary>
|
||||
private GameObject _buttonContainerObj = null!;
|
||||
/// <summary>Progress bar background (rounded, theme colors); shown when the download starts.</summary>
|
||||
private GameObject _progressBarObj = null!;
|
||||
/// <summary>Progress bar fill object.</summary>
|
||||
private GameObject _progressBarFillObj = null!;
|
||||
/// <summary>Fill rect; its <c>anchorMax.x</c> is the progress fraction 0..1 (see <see cref="UpdateProgress"/>).</summary>
|
||||
private RectTransform _fillRect = null!;
|
||||
/// <summary>Text centered on the progress bar (percentage or "Extracting...").</summary>
|
||||
private TextMeshProUGUI _progressText = null!;
|
||||
/// <summary>Status line below the progress bar; shown when the download starts.</summary>
|
||||
private GameObject _statusTextObj = null!;
|
||||
/// <summary>Status message, colored by state (gray starting, cyan downloading, yellow extracting, green done, red failed).</summary>
|
||||
private TextMeshProUGUI _statusText = null!;
|
||||
|
||||
/// <summary>O5Kit window holding the dialog contents; disposed in <see cref="OnDestroy"/>.</summary>
|
||||
private O5Window? _window;
|
||||
/// <summary>True from the Download click until the download fails; prevents a second concurrent download.</summary>
|
||||
private bool _isDownloading;
|
||||
|
||||
/// <summary>
|
||||
/// Archive URL for the current platform: a fixed BtbN FFmpeg-Builds autobuild (2026-05-31, FFmpeg master
|
||||
/// N-124714-g49a77d37be, GPL, shared libraries). Windows gets the win64 .zip; every other platform gets the
|
||||
/// linux64 .tar.xz (macOS never downloads, see <see cref="Start"/>).
|
||||
/// </summary>
|
||||
private string DownloadUrl => Application.platform switch
|
||||
{
|
||||
RuntimePlatform.WindowsPlayer => "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-05-31-13-22/ffmpeg-N-124714-g49a77d37be-win64-gpl-shared.zip",
|
||||
_ => "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-05-31-13-22/ffmpeg-N-124714-g49a77d37be-linux64-gpl-shared.tar.xz"
|
||||
};
|
||||
|
||||
/// <summary>Cancellation for the running download; created per download, cancelled by Cancel, disposed in OnDestroy.</summary>
|
||||
private System.Threading.CancellationTokenSource? _cts;
|
||||
/// <summary>Actions posted from background threads, run in order on the main thread by <see cref="Update"/>.</summary>
|
||||
private readonly System.Collections.Concurrent.ConcurrentQueue<Action> _mainThreadQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether UserLibs contains at least one file for each of the five FFmpeg libraries the mod uses
|
||||
/// (avcodec, avformat, avutil, swscale, swresample), matched by platform-specific file name patterns:
|
||||
/// <c>avcodec-*.dll</c> on Windows, <c>libavcodec.*.dylib</c> on macOS, <c>libavcodec.so*</c> elsewhere.
|
||||
/// Only file presence is checked, not the library version or whether it loads.
|
||||
/// </summary>
|
||||
/// <returns><c>false</c> when UserLibs does not exist or any library is missing.</returns>
|
||||
public static bool CheckFFmpegExists()
|
||||
{
|
||||
var userLibs = MelonEnvironment.UserLibsDirectory;
|
||||
@@ -68,6 +112,10 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the dialog on a new GameObject kept across scene loads, unless the libraries are already present.
|
||||
/// Main thread only.
|
||||
/// </summary>
|
||||
public static void ShowDownloadUI()
|
||||
{
|
||||
|
||||
@@ -78,8 +126,13 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
obj.AddComponent<FFmpegDownloadUI>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The open dialog, or <c>null</c> after it is destroyed. <c>FirstUpdateHook</c> waits while it is set and checks
|
||||
/// the libraries again after it closes.
|
||||
/// </summary>
|
||||
public static FFmpegDownloadUI? Instance { get; private set; }
|
||||
|
||||
/// <summary>Registers <see cref="Instance"/>; destroys the dialog again when the libraries already exist.</summary>
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
@@ -89,9 +142,13 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the canvas, backdrop and window: description, button row, and the hidden progress bar and status line.
|
||||
/// Layout values are canvas units at the 1920x1080 reference resolution.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
// Set UI layer (5)
|
||||
// Set UI layer (5, Unity's built-in "UI" layer)
|
||||
gameObject.layer = 5;
|
||||
|
||||
// Canvas setup
|
||||
@@ -99,7 +156,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 32767; // Topmost
|
||||
|
||||
// Responsive Canvas Scaler setup
|
||||
// Canvas Scaler: 1920x1080 reference resolution, width and height matched equally (0.5)
|
||||
var scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920, 1080);
|
||||
@@ -110,7 +167,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
|
||||
var ctx = RendererUIManager.Context;
|
||||
|
||||
// 1. Create full screen backdrop
|
||||
// 1. Create full screen backdrop: black at 65% opacity, blocks clicks to everything below
|
||||
var bgPanelObj = new GameObject("Backdrop");
|
||||
bgPanelObj.transform.SetParent(transform, false);
|
||||
bgPanelObj.layer = 5;
|
||||
@@ -125,7 +182,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
bgRect.offsetMin = Vector2.zero;
|
||||
bgRect.offsetMax = Vector2.zero;
|
||||
|
||||
// 2. Create actual native O5Window
|
||||
// 2. Create the O5Kit window: 580x270 units, draggable, fixed size, no close button (Cancel closes it).
|
||||
// The title is localized once here; the dialog does not follow later language changes.
|
||||
_window = O5Window.Create(ctx, bgPanelObj.transform, new O5WindowOptions
|
||||
{
|
||||
Id = "RendererMod_FFmpegDownloadDialog",
|
||||
@@ -138,6 +196,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
Padding = new RectOffset(16, 16, 16, 16)
|
||||
});
|
||||
|
||||
// Vertical stack: description (takes the remaining height), buttons (40), progress bar (24), status (22),
|
||||
// 14 units apart
|
||||
var content = _window.Content;
|
||||
|
||||
var vLayout = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
@@ -148,7 +208,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
vLayout.childForceExpandWidth = true;
|
||||
vLayout.childForceExpandHeight = false;
|
||||
|
||||
// 3. Description text
|
||||
// 3. Description text: "required" line, then either the macOS build/website lines or the download/size lines
|
||||
var descText = O5Factory.ControlText(ctx, content, 18f, noPad: true);
|
||||
var descStrBuilder = new StringBuilder();
|
||||
descStrBuilder.AppendLine(LocalizationManager.Get("ui.ffmpeg_download.desc_required"));
|
||||
@@ -172,7 +232,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
descLE.flexibleWidth = 1f;
|
||||
descLE.flexibleHeight = 1f;
|
||||
|
||||
// 4. Button container
|
||||
// 4. Button container: two equal-width 40-unit buttons, 10 units apart
|
||||
_buttonContainerObj = new GameObject("ButtonContainer");
|
||||
_buttonContainerObj.transform.SetParent(content, false);
|
||||
_buttonContainerObj.layer = 5;
|
||||
@@ -189,6 +249,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
btnLayout.childForceExpandWidth = true;
|
||||
btnLayout.childForceExpandHeight = true;
|
||||
|
||||
// Primary button: download and install (Windows/Linux), or open the FFmpeg website and close the dialog (macOS)
|
||||
Action onDownloadClick = Application.platform != RuntimePlatform.OSXPlayer
|
||||
? StartDownloadProcess
|
||||
: () => { Application.OpenURL("https://ffmpeg.org"); Destroy(gameObject); };
|
||||
@@ -220,7 +281,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
cancelBtnLE.flexibleWidth = 1f;
|
||||
cancelBtnLE.preferredHeight = 40f;
|
||||
|
||||
// 5. Progress bar container
|
||||
// 5. Progress bar container (hidden until the download starts): rounded background in the theme object color
|
||||
// with a rounded outline drawn over it
|
||||
_progressBarObj = new GameObject("ProgressBarContainer");
|
||||
_progressBarObj.transform.SetParent(content, false);
|
||||
_progressBarObj.layer = 5;
|
||||
@@ -251,7 +313,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
barOutlineRect.offsetMin = Vector2.zero;
|
||||
barOutlineRect.offsetMax = Vector2.zero;
|
||||
|
||||
// Progress bar fill image
|
||||
// Progress bar fill image: anchored at the left edge with zero size offset, so its width is set entirely by
|
||||
// anchorMax.x (0 = empty, 1 = full) in UpdateProgress
|
||||
_progressBarFillObj = new GameObject("ProgressBarFill");
|
||||
_progressBarFillObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
_progressBarFillObj.layer = 5;
|
||||
@@ -268,7 +331,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
_fillRect.anchoredPosition = Vector2.zero;
|
||||
_fillRect.sizeDelta = Vector2.zero;
|
||||
|
||||
// Progress text
|
||||
// Progress text: 13 pt white, centered over the bar
|
||||
var progressTextObj = new GameObject("ProgressBarText");
|
||||
progressTextObj.transform.SetParent(_progressBarObj.transform, false);
|
||||
progressTextObj.layer = 5;
|
||||
@@ -286,7 +349,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
progressTextRect.offsetMin = Vector2.zero;
|
||||
progressTextRect.offsetMax = Vector2.zero;
|
||||
|
||||
// 6. Status text
|
||||
// 6. Status text (hidden until the download starts): 14 pt, centered
|
||||
_statusTextObj = new GameObject("StatusText");
|
||||
_statusTextObj.transform.SetParent(content, false);
|
||||
_statusTextObj.layer = 5;
|
||||
@@ -310,6 +373,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
statusRect.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
/// <summary>Runs every action posted by the background download task, in posting order.</summary>
|
||||
private void Update()
|
||||
{
|
||||
// Execute queued actions on main thread
|
||||
@@ -319,11 +383,19 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts an action to run on the main thread during the next <see cref="Update"/>. Safe to call from any thread.
|
||||
/// Actions posted after the dialog is destroyed are never run.
|
||||
/// </summary>
|
||||
private void ExecuteOnMainThread(Action action)
|
||||
{
|
||||
_mainThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download button handler (Windows/Linux): switches the dialog to progress mode and starts the background task
|
||||
/// that downloads the archive, extracts the libraries and closes the dialog. Ignored while a download runs.
|
||||
/// </summary>
|
||||
private void StartDownloadProcess()
|
||||
{
|
||||
if (_isDownloading) return;
|
||||
@@ -336,7 +408,9 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
|
||||
_cts = new System.Threading.CancellationTokenSource();
|
||||
|
||||
// Start download in background thread
|
||||
// Start download in background thread. Everything inside runs on thread-pool threads: Unity objects are only
|
||||
// touched through UpdateStatus/UpdateProgress/ExecuteOnMainThread. The task is not awaited; its outcome is
|
||||
// reported through the status text.
|
||||
Task.Run(async () =>
|
||||
{
|
||||
string tempZipPath = "";
|
||||
@@ -351,6 +425,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
Directory.CreateDirectory(userLibsDir);
|
||||
}
|
||||
|
||||
// Temporary archive path in UserLibs: ffmpeg_temp.zip (Windows, macOS) or ffmpeg_temp.tar.xz (others).
|
||||
// It is deleted after extraction (ExtractDllsAsync) or on cancellation.
|
||||
string extension = Application.platform switch
|
||||
{
|
||||
RuntimePlatform.WindowsPlayer => ".zip",
|
||||
@@ -359,6 +435,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
};
|
||||
tempZipPath = Path.Combine(userLibsDir, "ffmpeg_temp" + extension);
|
||||
|
||||
// percent is the downloaded fraction 0..1. Progress<T> created on a thread-pool thread (no
|
||||
// SynchronizationContext) invokes this callback on thread-pool threads; it only posts UI updates.
|
||||
var progressReporter = new Progress<float>(percent =>
|
||||
{
|
||||
UpdateProgress(percent, $"{percent * 100f:F1}%");
|
||||
@@ -367,7 +445,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
|
||||
await DownloadFileAsync(DownloadUrl, tempZipPath, progressReporter, _cts.Token);
|
||||
|
||||
// Extract after download complete
|
||||
// Extract after download complete; the bar is held at 99% while extracting
|
||||
UpdateStatus(LocalizationManager.Get("ui.ffmpeg_download.status_extracting"), new Color(0.9f, 0.9f, 0.2f));
|
||||
UpdateProgress(0.99f, LocalizationManager.Get("ui.ffmpeg_download.progress_extracting"));
|
||||
|
||||
@@ -377,9 +455,10 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
UpdateStatus(LocalizationManager.Get("ui.ffmpeg_download.status_complete"), new Color(0.2f, 0.9f, 0.2f));
|
||||
UpdateProgress(1f, "100%");
|
||||
|
||||
// Keep the "complete" message visible for 1000 ms before closing
|
||||
await Task.Delay(1000);
|
||||
|
||||
// Destroy UI on main thread
|
||||
// Destroy UI on main thread. FirstUpdateHook then sees Instance == null and checks the libraries again.
|
||||
ExecuteOnMainThread(() =>
|
||||
{
|
||||
Destroy(gameObject);
|
||||
@@ -387,7 +466,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Clean up temporary file on cancellation
|
||||
// Clean up temporary file on cancellation. Cancellation comes from CancelProcess, which has already
|
||||
// destroyed the dialog, so the status update below is posted but never shown.
|
||||
if (!string.IsNullOrEmpty(tempZipPath) && File.Exists(tempZipPath))
|
||||
{
|
||||
try { File.Delete(tempZipPath); }
|
||||
@@ -403,7 +483,9 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
Melon<RenderMod>.Logger.Error($"Error during FFmpeg download/installation: {ex}");
|
||||
UpdateStatus(LocalizationManager.Get("ui.ffmpeg_download.status_failed", ex.Message), new Color(1f, 0.3f, 0.3f));
|
||||
|
||||
// Reset buttons on failure
|
||||
// Reset buttons on failure so the user can retry (Download) or close the dialog (Cancel).
|
||||
// The label is set to "Retry" only if a child at "DownloadButton/DownloadText" exists. The button made by
|
||||
// O5Factory.Button in Start is a GameObject named "O5Button", which this path does not match.
|
||||
ExecuteOnMainThread(() =>
|
||||
{
|
||||
_isDownloading = false;
|
||||
@@ -418,6 +500,10 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel button handler: requests cancellation of a running download and destroys the dialog immediately.
|
||||
/// An extraction already in progress is not cancellable and finishes in the background.
|
||||
/// </summary>
|
||||
private void CancelProcess()
|
||||
{
|
||||
if (_isDownloading)
|
||||
@@ -427,6 +513,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>Posts a status text and color change to the main thread. Callable from any thread.</summary>
|
||||
private void UpdateStatus(string message, Color color)
|
||||
{
|
||||
ExecuteOnMainThread(() =>
|
||||
@@ -439,6 +526,9 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Posts a progress bar update to the main thread. Callable from any thread.</summary>
|
||||
/// <param name="percent">Filled fraction 0..1 (not 0..100); becomes the fill's <c>anchorMax.x</c>.</param>
|
||||
/// <param name="text">Text shown on the bar.</param>
|
||||
private void UpdateProgress(float percent, string text)
|
||||
{
|
||||
ExecuteOnMainThread(() =>
|
||||
@@ -454,15 +544,28 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams the HTTP response body of <paramref name="url"/> to <paramref name="destinationPath"/> (created or
|
||||
/// overwritten) in 8 KiB chunks. Runs on a thread-pool thread.
|
||||
/// </summary>
|
||||
/// <param name="url">Archive URL.</param>
|
||||
/// <param name="destinationPath">Output file path.</param>
|
||||
/// <param name="progress">
|
||||
/// Receives the downloaded fraction 0..1 after every chunk; not reported when the server sends no Content-Length.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">Cancels the request and the copy loop (<see cref="OperationCanceledException"/>).</param>
|
||||
/// <exception cref="HttpRequestException">The server returned a non-success status code.</exception>
|
||||
private async Task DownloadFileAsync(string url, string destinationPath, IProgress<float> progress, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
// Add User-Agent header to prevent blocking
|
||||
// Add User-Agent header to prevent blocking by servers that reject requests without one
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ADOFAIRenderer/1.0");
|
||||
|
||||
// ResponseHeadersRead returns after the headers, so the body is streamed instead of buffered in memory
|
||||
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
// Total size from Content-Length, or -1 when unknown. File stream: 8192-byte buffer, asynchronous I/O.
|
||||
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync();
|
||||
await using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
|
||||
@@ -483,12 +586,28 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the shared libraries from the downloaded archive into <paramref name="outputDirectory"/>, overwriting
|
||||
/// files of the same name, and deletes the archive afterwards (also on failure; a failed delete is logged as a
|
||||
/// warning). Runs on thread-pool threads and is not cancellable.
|
||||
/// <list type="bullet">
|
||||
/// <item>Windows/macOS (.zip): every entry ending in .dll (Windows) or .dylib (macOS) is extracted flat, by its file
|
||||
/// name only, ignoring its folder inside the archive.</item>
|
||||
/// <item>Other platforms (.tar.xz): the archive is unpacked with the system <c>tar</c> command into the temporary
|
||||
/// folder "ffmpeg_temp_extracted" in <paramref name="outputDirectory"/>; every file matching <c>*.so*</c> in any
|
||||
/// subfolder is copied flat to <paramref name="outputDirectory"/>, then the temporary folder is deleted.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="zipPath">Downloaded archive path (.zip or .tar.xz despite the parameter name).</param>
|
||||
/// <param name="outputDirectory">UserLibs folder.</param>
|
||||
/// <exception cref="Exception"><c>tar</c> exited with a non-zero code (message includes its standard error).</exception>
|
||||
private async Task ExtractDllsAsync(string zipPath, string outputDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Application.platform == RuntimePlatform.OSXPlayer || Application.platform == RuntimePlatform.WindowsPlayer)
|
||||
{
|
||||
// .zip: extract matching library entries flat into outputDirectory
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var targetExtension = Application.platform == RuntimePlatform.OSXPlayer ? ".dylib" : ".dll";
|
||||
@@ -522,6 +641,7 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
Directory.CreateDirectory(tempExtractDir);
|
||||
|
||||
// tar receives forward-slash paths, quoted for spaces: tar -xf "<archive>" -C "<temp dir>"
|
||||
var archiveUnixPath = zipPath.Replace('\\', '/');
|
||||
var tempExtractUnixDir = tempExtractDir.Replace('\\', '/');
|
||||
|
||||
@@ -545,7 +665,8 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// Find .so files and copy to outputDirectory
|
||||
// Find .so files (including versioned names such as libavcodec.so.<major>) and copy them flat to
|
||||
// outputDirectory
|
||||
var soFiles = Directory.GetFiles(tempExtractDir, "*.so*", SearchOption.AllDirectories);
|
||||
foreach (var soFile in soFiles)
|
||||
{
|
||||
@@ -582,6 +703,10 @@ internal class FFmpegDownloadUI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the window and the cancellation source and clears <see cref="Instance"/>. A download that was not
|
||||
/// cancelled keeps running in the background; its UI updates are posted but no longer drained.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
_window?.Dispose();
|
||||
|
||||
+36
-2
@@ -8,15 +8,25 @@ namespace ADOFAIRenderer.UI;
|
||||
/// <summary>
|
||||
/// Component that binds a TextMeshProUGUI component to a localization key or dynamic string provider,
|
||||
/// automatically updating the displayed text whenever the active language changes.
|
||||
/// <para>
|
||||
/// The text is set when the binding is made (<see cref="Bind(TextMeshProUGUI, string)"/>), when the component is enabled,
|
||||
/// and on every <see cref="LocalizationManager.OnLanguageChanged"/> event. A key and a dynamic provider are mutually
|
||||
/// exclusive: setting one clears the other. With neither set, <see cref="Refresh"/> leaves the text unchanged.
|
||||
/// Runs on the Unity main thread.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class LocalizedText : MonoBehaviour
|
||||
{
|
||||
/// <summary>Text component that receives the string; looked up on the same GameObject if not assigned by Bind.</summary>
|
||||
private TextMeshProUGUI? _textComponent;
|
||||
/// <summary>Localization key resolved through <see cref="LocalizationManager.Get(string)"/>; <c>null</c> when a provider is used.</summary>
|
||||
private string? _key;
|
||||
/// <summary>Function that returns the full text (already localized/formatted); <c>null</c> when a key is used.</summary>
|
||||
private Func<string>? _dynamicProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the localization key. Setting this will clear any dynamic provider.
|
||||
/// Gets or sets the localization key. Setting this will clear any dynamic provider and refresh the text immediately.
|
||||
/// A null or empty key leaves the current text as it is.
|
||||
/// </summary>
|
||||
public string? Key
|
||||
{
|
||||
@@ -30,8 +40,11 @@ internal class LocalizedText : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a dynamic string provider function.
|
||||
/// Sets a dynamic string provider function, clears the key and refreshes the text immediately.
|
||||
/// The provider is called on every refresh (enable and language change), so it can combine several keys or
|
||||
/// runtime values, e.g. a label that depends on UI state.
|
||||
/// </summary>
|
||||
/// <param name="provider">Function returning the text to display; called on the Unity main thread.</param>
|
||||
public void SetDynamic(Func<string> provider)
|
||||
{
|
||||
_dynamicProvider = provider;
|
||||
@@ -39,27 +52,35 @@ internal class LocalizedText : MonoBehaviour
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the text component and subscribes to language changes. The subscription lasts until <see cref="OnDestroy"/>,
|
||||
/// also while the component is disabled (a refresh of a disabled object only changes its text).
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
EnsureComponent();
|
||||
LocalizationManager.OnLanguageChanged += HandleLanguageChanged;
|
||||
}
|
||||
|
||||
/// <summary>Refreshes the text each time the component is enabled.</summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from <see cref="LocalizationManager.OnLanguageChanged"/>.</summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
LocalizationManager.OnLanguageChanged -= HandleLanguageChanged;
|
||||
}
|
||||
|
||||
/// <summary>Language change handler; the new language is read by <see cref="LocalizationManager.Get(string)"/>.</summary>
|
||||
private void HandleLanguageChanged(SystemLanguage language)
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Assigns <see cref="_textComponent"/> from the same GameObject if it is not set yet.</summary>
|
||||
private void EnsureComponent()
|
||||
{
|
||||
if (_textComponent == null)
|
||||
@@ -70,6 +91,8 @@ internal class LocalizedText : MonoBehaviour
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the text using either the dynamic provider or the static localization key.
|
||||
/// The provider takes precedence; with neither set, or without a text component, nothing changes.
|
||||
/// A key that is missing in every locale is displayed as the key itself (see <see cref="LocalizationManager.Get(string)"/>).
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
@@ -88,7 +111,13 @@ internal class LocalizedText : MonoBehaviour
|
||||
|
||||
/// <summary>
|
||||
/// Binds a TextMeshProUGUI component to a localization key.
|
||||
/// Reuses the <see cref="LocalizedText"/> already on the target's GameObject or adds one, then sets the key,
|
||||
/// which replaces any earlier binding and updates the text immediately.
|
||||
/// </summary>
|
||||
/// <param name="target">Text to localize.</param>
|
||||
/// <param name="key">Localization key, e.g. <c>"ui.render_config.codec"</c>.</param>
|
||||
/// <returns>The binding component.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="target"/> is null.</exception>
|
||||
public static LocalizedText Bind(TextMeshProUGUI target, string key)
|
||||
{
|
||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||
@@ -100,7 +129,12 @@ internal class LocalizedText : MonoBehaviour
|
||||
|
||||
/// <summary>
|
||||
/// Binds a TextMeshProUGUI component to a dynamic string provider function.
|
||||
/// Reuses or adds the component like <see cref="Bind(TextMeshProUGUI, string)"/> and updates the text immediately.
|
||||
/// </summary>
|
||||
/// <param name="target">Text to localize.</param>
|
||||
/// <param name="provider">Function returning the text; called on every refresh.</param>
|
||||
/// <returns>The binding component.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="target"/> is null.</exception>
|
||||
public static LocalizedText Bind(TextMeshProUGUI target, Func<string> provider)
|
||||
{
|
||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||
|
||||
@@ -8,9 +8,20 @@ namespace ADOFAIRenderer.UI;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Assets loaded from the mod's embedded asset bundles (fonts and the color conversion compute shader).
|
||||
/// <para>
|
||||
/// <see cref="Load"/> is called once from <c>RenderMod.OnInitializeMelon</c>; the fields stay <c>null</c> until then.
|
||||
/// Bundles are obtained from <c>BundleLoader.GetBundle</c> by name ("fonts", "shaders"). Main thread only.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
internal static class ModAssets
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the font and shader bundles and the assets inside them, logging after each bundle.
|
||||
/// Exceptions from bundle or asset loading are not caught here and propagate to the caller.
|
||||
/// </summary>
|
||||
public static void Load()
|
||||
{
|
||||
FontBundle = BundleLoader.GetBundle("fonts");
|
||||
@@ -23,24 +34,49 @@ internal static class ModAssets
|
||||
Melon<RenderMod>.Logger.Msg("Shader loaded successfully.");
|
||||
}
|
||||
|
||||
/// <summary>Asset bundle "fonts" holding the TextMeshPro font assets.</summary>
|
||||
public static AssetBundle FontBundle = null!;
|
||||
/// <summary>Asset bundle "shaders" holding the compute shaders.</summary>
|
||||
public static AssetBundle ShaderBundle = null!;
|
||||
|
||||
/// <summary>
|
||||
/// NanumSquare Neo, asset "NanumSquareNeoOTF-Bd SDF" (Bd = Bold weight). Used as the regular UI font of the mod
|
||||
/// (O5Kit <see cref="RendererFontProvider.Regular"/>, DebugMonitorUI text).
|
||||
/// </summary>
|
||||
public static TMP_FontAsset NanumSquareNeoNormal = null!;
|
||||
/// <summary>
|
||||
/// NanumSquare Neo, asset "NanumSquareNeoOTF-Eb SDF" (Eb = ExtraBold weight). Used as the O5Kit medium font
|
||||
/// (<see cref="RendererFontProvider.Medium"/>).
|
||||
/// </summary>
|
||||
public static TMP_FontAsset NanumSquareNeoBold = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Compute shader "ColorConversion" that converts captured frames to the encoder's YUV pixel format on the GPU
|
||||
/// (used by FrameReader).
|
||||
/// </summary>
|
||||
public static ComputeShader ColorConversionShader = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// O5Kit font provider for the mod's UI: the bundled NanumSquare Neo fonts for regular and medium text, and O5Kit's
|
||||
/// default monospace font.
|
||||
/// </summary>
|
||||
internal sealed class RendererFontProvider : IFontProvider
|
||||
{
|
||||
/// <summary>O5Kit default provider, used only for <see cref="Monospace"/>.</summary>
|
||||
private static readonly DefaultFontProvider DefaultFontProvider = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>Regular text font: <see cref="ModAssets.NanumSquareNeoNormal"/>.</summary>
|
||||
public TMP_FontAsset Regular => ModAssets.NanumSquareNeoNormal;
|
||||
/// <summary>Medium (emphasis) text font: <see cref="ModAssets.NanumSquareNeoBold"/>.</summary>
|
||||
public TMP_FontAsset Medium => ModAssets.NanumSquareNeoBold;
|
||||
/// <summary>Monospace font (e.g. hex color input): O5Kit's default monospace font.</summary>
|
||||
public TMP_FontAsset Monospace => DefaultFontProvider.Monospace;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the provider disposed. The font assets belong to <see cref="ModAssets"/> and are not unloaded.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
@@ -48,19 +84,39 @@ internal sealed class RendererFontProvider : IFontProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owner of the mod's O5Kit UI: the shared <see cref="O5Context"/> (config, theme, fonts) and the window manager
|
||||
/// with its canvas.
|
||||
/// <para>
|
||||
/// Added once by <c>RenderMod.OnInitializeMelon</c> to a GameObject kept across scene loads. Other UI code reads
|
||||
/// <see cref="Context"/> and <see cref="WindowManager"/> through the static <see cref="Instance"/>.
|
||||
/// <see cref="Update"/> drives O5Kit's per-frame work (object ticks, tooltip, keyboard shortcuts). Main thread only.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class RendererUIManager : MonoBehaviour
|
||||
{
|
||||
/// <summary>The single instance, assigned in <see cref="Awake"/>.</summary>
|
||||
public static RendererUIManager Instance = null!;
|
||||
|
||||
/// <summary>Shared O5Kit context used to build every mod window and dialog.</summary>
|
||||
public static O5Context Context => Instance._context;
|
||||
/// <summary>Window manager whose canvas holds the mod windows (e.g. RenderConfigWindow).</summary>
|
||||
public O5WindowManager WindowManager => _windowManager;
|
||||
|
||||
private O5Context _context = null!;
|
||||
private O5WindowManager _windowManager = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the O5Kit context with the mod theme and fonts, then the window manager and its canvas
|
||||
/// ("RendererMod_Canvas"), and lowers that canvas's sorting order to <see cref="WindowCanvasSortingOrder"/>.
|
||||
/// </summary>
|
||||
private void Awake() {
|
||||
Instance = this;
|
||||
|
||||
// O5Kit config: UI scale 1.0, middle click resets a control to its default value, slider drag sensitivity 1.0.
|
||||
// Theme: O5Kit's dark theme with the mod colors. Colors are RGBA 0..1; the accent color
|
||||
// (1.000, 0.294, 0.079) is orange-red (#FF4B14 approx.). Sizes are canvas units: corner radius 12,
|
||||
// outline width 2, control height 40, body font 18, H1 font 48.
|
||||
_context = new O5Context(
|
||||
config: new O5Config {
|
||||
UIScale = 1.0f,
|
||||
@@ -101,14 +157,20 @@ internal class RendererUIManager : MonoBehaviour
|
||||
if (windowCanvas != null) windowCanvas.sortingOrder = WindowCanvasSortingOrder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorting order of the window canvas: below CommonWarningDialogUI (32766) and FFmpegDownloadUI (32767), above the
|
||||
/// render overlays (DebugMonitorUI 9999, EscapeHandlerUI 10000).
|
||||
/// </summary>
|
||||
private const int WindowCanvasSortingOrder = 32000;
|
||||
|
||||
/// <summary>Runs O5Kit's per-frame updates: object ticks, tooltip, and keyboard shortcut handling.</summary>
|
||||
private void Update() {
|
||||
O5Object.TickAll();
|
||||
Context.Tooltip.Tick();
|
||||
O5Kit.Input.O5ShortcutManager.HandleUpdate();
|
||||
}
|
||||
|
||||
/// <summary>Disposes the window manager, then the context.</summary>
|
||||
private void OnDestroy() {
|
||||
_windowManager.Dispose();
|
||||
_context.Dispose();
|
||||
|
||||
+444
-17
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user