using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: IgnoresAccessChecksTo("")] [assembly: AssemblyCompany("BatteryDie")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+ae3e2b15ff7bb4dcb16b001e239f837ad14ed2bd")] [assembly: AssemblyProduct("Captionman")] [assembly: AssemblyTitle("Captionman")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Captionman { [BepInPlugin("BatteryDie.Captionman", "Captionman", "1.0.0")] public class Captionman : BaseUnityPlugin { private GameAudioCaptionService? _gameAudioService; internal static Captionman Instance { get; private set; } internal static ManualLogSource Logger => Instance._logger; private ManualLogSource _logger => ((BaseUnityPlugin)this).Logger; internal Harmony? Harmony { get; set; } internal ConfigEntry EnableCaptionsUI { get; private set; } internal ConfigEntry GameAudioCaptions { get; private set; } internal ConfigEntry GameAudioRepeatCooldownSeconds { get; private set; } internal ConfigEntry GameAudioCaptionFile { get; private set; } internal ConfigEntry BackgroundOpacity { get; private set; } internal ConfigEntry TextSize { get; private set; } internal ConfigEntry DisableTextColour { get; private set; } internal ConfigEntry TextLeftAlign { get; private set; } internal ConfigEntry HorizontalPosition { get; private set; } internal ConfigEntry VerticalPosition { get; private set; } internal ConfigEntry EnableDebug { get; private set; } internal GameAudioCaptionService? GameAudioService => _gameAudioService; private void Awake() { //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Expected O, but got Unknown //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Expected O, but got Unknown //IL_025f: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Expected O, but got Unknown //IL_02f1: Unknown result type (might be due to invalid IL or missing references) //IL_02fb: Expected O, but got Unknown Instance = this; ((Component)this).gameObject.transform.parent = null; ((Object)((Component)this).gameObject).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); EnableCaptionsUI = ((BaseUnityPlugin)this).Config.Bind("Captions", "EnableCaptionsUI", true, "Master toggle for caption rendering across the entire game"); GameAudioCaptions = ((BaseUnityPlugin)this).Config.Bind("Captions", "GameAudioCaptions", true, "Enable closed captions for game audio"); GameAudioRepeatCooldownSeconds = ((BaseUnityPlugin)this).Config.Bind("Captions", "GameAudioRepeatCooldownSeconds", 4f, new ConfigDescription("Minimum cooldown in seconds before the same game-audio caption text can appear again", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 10f), Array.Empty())); GameAudioCaptionFile = ((BaseUnityPlugin)this).Config.Bind("Captions", "GameAudioCaptionFile", "captionsEN.csv", "Caption CSV filename to load. If not found, captionsEN.csv is used as fallback."); BackgroundOpacity = ((BaseUnityPlugin)this).Config.Bind("Appearance", "BackgroundOpacity", 0.7f, new ConfigDescription("Opacity of caption background from 0.0 (transparent) to 1.0 (opaque)", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); if (BackgroundOpacity.Value < 0f || BackgroundOpacity.Value > 1f) { BackgroundOpacity.Value = Mathf.Clamp01(BackgroundOpacity.Value); ((BaseUnityPlugin)this).Config.Save(); } TextSize = ((BaseUnityPlugin)this).Config.Bind("Appearance", "TextSize", 16f, new ConfigDescription("Caption font size from 10.0 to 25.0", (AcceptableValueBase)(object)new AcceptableValueRange(10f, 25f), Array.Empty())); if (TextSize.Value < 10f || TextSize.Value > 25f) { TextSize.Value = Mathf.Clamp(TextSize.Value, 10f, 25f); ((BaseUnityPlugin)this).Config.Save(); } DisableTextColour = ((BaseUnityPlugin)this).Config.Bind("Appearance", "DisableTextColour", false, "Disable custom text colour tags (for example Alert)"); TextLeftAlign = ((BaseUnityPlugin)this).Config.Bind("Appearance", "TextLeftAlign", false, "Align caption text to the left instead of centered"); HorizontalPosition = ((BaseUnityPlugin)this).Config.Bind("Appearance", "HorizontalPosition", 0f, new ConfigDescription("Horizontal position offset for captions", (AcceptableValueBase)(object)new AcceptableValueRange(-270f, 260f), Array.Empty())); if (HorizontalPosition.Value < -270f || HorizontalPosition.Value > 260f) { HorizontalPosition.Value = Mathf.Clamp(HorizontalPosition.Value, -270f, 260f); ((BaseUnityPlugin)this).Config.Save(); } VerticalPosition = ((BaseUnityPlugin)this).Config.Bind("Appearance", "VerticalPosition", 50f, new ConfigDescription("Vertical position offset for captions", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 350f), Array.Empty())); if (VerticalPosition.Value < 0f || VerticalPosition.Value > 350f) { VerticalPosition.Value = Mathf.Clamp(VerticalPosition.Value, 0f, 350f); ((BaseUnityPlugin)this).Config.Save(); } EnableDebug = ((BaseUnityPlugin)this).Config.Bind("Developer", "EnableDebug", false, "Enable debug logging for troubleshooting"); GameAudioCaptionFile.SettingChanged += delegate { SoundCaptionCatalog.ReloadFromConfig(); }; SoundCaptionCatalog.ReloadFromConfig(); _gameAudioService = new GameAudioCaptionService(this); CaptionUI.EnsureInstance(); Patch(); Logger.LogInfo((object)$"{((BaseUnityPlugin)this).Info.Metadata.GUID} v{((BaseUnityPlugin)this).Info.Metadata.Version} has loaded!"); } internal void Patch() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_0025: Expected O, but got Unknown if (Harmony == null) { Harmony val = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID); Harmony val2 = val; Harmony = val; } Harmony.PatchAll(); } internal void Unpatch() { Harmony? harmony = Harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void Update() { CaptionUI.EnsureInstance(); } internal static void LogInfo(string message) { Logger.LogInfo((object)(message ?? "")); } internal static void LogWarning(string message) { Logger.LogWarning((object)(message ?? "")); } internal static void LogError(string message) { Logger.LogError((object)(message ?? "")); } internal static void LogDebug(string message) { if (Instance.EnableDebug.Value) { Logger.LogInfo((object)("[Debug] " + message)); } } internal static void LogOutput(string playerName, string text) { Logger.LogInfo((object)(playerName + ": " + text)); } } public static class CaptionmanApi { public static bool SendCaption(string text) { return CaptionUI.AddSystemCaptionSafe(text); } public static bool SendCaption(string speaker, string text) { return CaptionUI.AddSpeakerCaptionSafe(speaker, text); } } public class CaptionUI : MonoBehaviour { internal enum CaptionKind { Speaker, GameAudio, System } private class CaptionEntry { public string Speaker { get; set; } = string.Empty; public string Text { get; set; } = string.Empty; public float Timestamp { get; set; } public float DisplayDuration { get; set; } public CaptionKind Kind { get; set; } } private class PendingCaption { public string Speaker { get; set; } = string.Empty; public string Text { get; set; } = string.Empty; public CaptionKind Kind { get; set; } } private static readonly Queue PendingCaptions = new Queue(); private const int MaxPendingCaptions = 40; private readonly Queue _captionQueue = new Queue(); private const int MaxCaptions = 6; private const float MinDisplayDuration = 3f; private const float MaxDisplayDuration = 14f; private static readonly Regex WhitespaceRegex = new Regex("\\s+", RegexOptions.Compiled); private static readonly Regex ColorOpenTagRegex = new Regex("]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ColorCloseTagRegex = new Regex("", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Dictionary ApprovedTextColors = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["red"] = "#FF5A5A", ["yellow"] = "#FFD85A", ["green"] = "#6DDB7B", ["blue"] = "#73B7FF", ["cyan"] = "#66E0FF", ["orange"] = "#FFAD5A", ["pink"] = "#FF7CC8", ["white"] = "#FFFFFF", ["gray"] = "#B8B8B8", ["grey"] = "#B8B8B8" }; private const float MaxPanelWidth = 500f; private const float MinPanelWidth = 180f; private const float MinPanelHeight = 38f; private const float MaxPanelHeight = 260f; private GameObject? _uiContainer; private RectTransform? _containerRect; private Image? _backgroundPanel; private TextMeshProUGUI? _captionText; private CanvasGroup? _canvasGroup; private readonly Color _backgroundBaseColor = new Color(0f, 0f, 0f, 1f); private readonly Color _textColor = new Color(1f, 1f, 1f, 1f); private const float DefaultFontSize = 16f; private const float PanelPadding = 10f; private float _lastAppliedOpacity = -1f; private float _lastAppliedFontSize = -1f; private bool? _lastAppliedTextLeftAlign; private float _lastAppliedHorizontalPosition = float.NaN; private float _lastAppliedVerticalPosition = float.NaN; public static CaptionUI? Instance { get; private set; } internal static void EnsureInstance() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown if ((Object)(object)Instance != (Object)null) { return; } try { CaptionUI captionUI = Object.FindObjectOfType(); if ((Object)(object)captionUI != (Object)null) { Instance = captionUI; return; } GameObject val = new GameObject("CaptionUI"); val.AddComponent(); } catch (Exception ex) { Captionman.LogWarning("Failed to create Caption UI: " + ex.Message); } } public static bool AddSpeakerCaptionSafe(string speaker, string text) { return AddCaptionSafe(speaker, text, CaptionKind.Speaker); } public static bool AddSystemCaptionSafe(string text) { return AddCaptionSafe(string.Empty, text, CaptionKind.System); } internal static bool AddGameAudioCaptionSafe(string text) { return AddCaptionSafe(string.Empty, text, CaptionKind.GameAudio); } private static bool AddCaptionSafe(string speaker, string text, CaptionKind kind) { if (kind == CaptionKind.Speaker && string.IsNullOrWhiteSpace(speaker)) { return false; } if (string.IsNullOrWhiteSpace(text)) { return false; } EnsureInstance(); if ((Object)(object)Instance != (Object)null) { Instance.EnqueueCaption(speaker, text, kind); return true; } PendingCaptions.Enqueue(new PendingCaption { Speaker = speaker, Text = text, Kind = kind }); while (PendingCaptions.Count > 40) { PendingCaptions.Dequeue(); } return false; } private void Awake() { if ((Object)(object)Instance != (Object)null && (Object)(object)Instance != (Object)(object)this) { Object.Destroy((Object)(object)((Component)this).gameObject); return; } Instance = this; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); TryInitializeUI(); FlushPendingCaptions(); } private void Update() { if ((Object)(object)_uiContainer == (Object)null || (Object)(object)_captionText == (Object)null) { TryInitializeUI(); FlushPendingCaptions(); } if ((Object)(object)_uiContainer == (Object)null) { return; } ApplyBackgroundOpacity(); ApplyFontSize(); ApplyTextAlignment(); ApplyContainerPosition(); if ((Object)(object)Captionman.Instance == (Object)null || !Captionman.Instance.EnableCaptionsUI.Value) { _uiContainer.SetActive(false); return; } CleanupOldCaptions(); if (_captionQueue.Count == 0) { _uiContainer.SetActive(false); return; } _uiContainer.SetActive(true); UpdateCaptionDisplay(); } private bool TryInitializeUI() { if ((Object)(object)_uiContainer != (Object)null) { return true; } RectTransform val = ResolveOrCreateParentCanvasRect(); if ((Object)(object)val == (Object)null) { return false; } Initialize(val); return (Object)(object)_uiContainer != (Object)null; } private RectTransform? ResolveOrCreateParentCanvasRect() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown if ((Object)(object)HUDCanvas.instance != (Object)null && (Object)(object)HUDCanvas.instance.rect != (Object)null) { ((Component)this).transform.SetParent(((Component)HUDCanvas.instance).transform, false); return HUDCanvas.instance.rect; } GameObject val = new GameObject("CaptionmanOverlayCanvas"); val.transform.SetParent(((Component)this).transform, false); Canvas val2 = val.AddComponent(); val2.renderMode = (RenderMode)0; val2.sortingOrder = 2000; val.AddComponent(); val.AddComponent(); return val.GetComponent(); } private void Initialize(RectTransform parentRect) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Expected O, but got Unknown //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Expected O, but got Unknown //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_0192: Unknown result type (might be due to invalid IL or missing references) //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_01e0: Unknown result type (might be due to invalid IL or missing references) _uiContainer = new GameObject("CaptionUIContainer"); _containerRect = _uiContainer.AddComponent(); ((Transform)_containerRect).SetParent((Transform)(object)parentRect, false); _containerRect.anchorMin = new Vector2(0.5f, 0f); _containerRect.anchorMax = new Vector2(0.5f, 0f); _containerRect.pivot = new Vector2(0.5f, 0f); _containerRect.anchoredPosition = new Vector2(0f, 50f); _containerRect.sizeDelta = new Vector2(500f, 120f); _canvasGroup = _uiContainer.AddComponent(); _canvasGroup.alpha = 1f; GameObject val = new GameObject("CaptionPanel"); RectTransform val2 = val.AddComponent(); ((Transform)val2).SetParent((Transform)(object)_containerRect, false); val2.anchorMin = new Vector2(0f, 0f); val2.anchorMax = new Vector2(1f, 1f); val2.offsetMin = Vector2.zero; val2.offsetMax = Vector2.zero; _backgroundPanel = val.AddComponent(); ApplyBackgroundOpacity(force: true); GameObject val3 = new GameObject("CaptionText"); RectTransform val4 = val3.AddComponent(); ((Transform)val4).SetParent((Transform)(object)val2, false); val4.anchorMin = new Vector2(0f, 0f); val4.anchorMax = new Vector2(1f, 1f); val4.offsetMin = new Vector2(10f, 10f); val4.offsetMax = new Vector2(-10f, -10f); _captionText = val3.AddComponent(); ((TMP_Text)_captionText).alignment = (TextAlignmentOptions)1026; ((TMP_Text)_captionText).fontStyle = (FontStyles)1; ((Graphic)_captionText).color = _textColor; ((TMP_Text)_captionText).enableWordWrapping = true; ((TMP_Text)_captionText).overflowMode = (TextOverflowModes)0; ApplyFontSize(force: true); TrySetGameFont(); _uiContainer.SetActive(false); Captionman.LogDebug("Caption UI initialized"); } private void TrySetGameFont() { if ((Object)(object)_captionText == (Object)null) { return; } try { GameObject val = GameObject.Find("Tax Haul"); if (!((Object)(object)val == (Object)null)) { TMP_Text component = val.GetComponent(); if ((Object)(object)component != (Object)null && (Object)(object)component.font != (Object)null) { ((TMP_Text)_captionText).font = component.font; } } } catch (Exception ex) { Captionman.LogDebug("Unable to apply game font: " + ex.Message); } } private void EnqueueCaption(string speaker, string text, CaptionKind kind) { _captionQueue.Enqueue(new CaptionEntry { Speaker = speaker, Text = text, Timestamp = Time.time, DisplayDuration = ComputeReadDurationSeconds(speaker, text, kind), Kind = kind }); while (_captionQueue.Count > 6) { _captionQueue.Dequeue(); } } private void FlushPendingCaptions() { if (!((Object)(object)Instance != (Object)(object)this)) { while (PendingCaptions.Count > 0) { PendingCaption pendingCaption = PendingCaptions.Dequeue(); EnqueueCaption(pendingCaption.Speaker, pendingCaption.Text, pendingCaption.Kind); } } } private void CleanupOldCaptions() { float time = Time.time; while (_captionQueue.Count > 0) { CaptionEntry captionEntry = _captionQueue.Peek(); if (!(time - captionEntry.Timestamp < captionEntry.DisplayDuration)) { _captionQueue.Dequeue(); continue; } break; } } private void UpdateCaptionDisplay() { if ((Object)(object)_captionText == (Object)null || (Object)(object)_canvasGroup == (Object)null) { return; } List list = new List(_captionQueue.Count); foreach (CaptionEntry item in _captionQueue) { switch (item.Kind) { case CaptionKind.GameAudio: list.Add(TransformColorTags(item.Text)); break; case CaptionKind.Speaker: list.Add("" + TransformColorTags(item.Speaker) + ": " + TransformColorTags(item.Text)); break; default: list.Add(TransformColorTags(item.Text)); break; } } ((TMP_Text)_captionText).text = string.Join("\n", list); UpdatePanelSize(); _canvasGroup.alpha = 1f; } private static float ComputeReadDurationSeconds(string speaker, string text, CaptionKind kind) { string text2 = StripCustomColorTags(text); string text3 = StripCustomColorTags(speaker); string text4 = ((kind == CaptionKind.Speaker && !string.IsNullOrWhiteSpace(speaker)) ? (text3 + ": " + text2) : text2); if (string.IsNullOrWhiteSpace(text4)) { return 3f; } string text5 = WhitespaceRegex.Replace(text4.Trim(), " "); int length = text5.Length; string[] array = text5.Split(' ', StringSplitOptions.RemoveEmptyEntries); int num = array.Length; float num2 = (float)num / 3f; float num3 = (float)length / 14f; float num4 = 1.5f + Mathf.Max(num2, num3); return Mathf.Clamp(num4, 3f, 14f); } private static string StripCustomColorTags(string text) { if (string.IsNullOrEmpty(text)) { return string.Empty; } string input = ColorOpenTagRegex.Replace(text, string.Empty); return ColorCloseTagRegex.Replace(input, string.Empty); } private static string TransformColorTags(string text) { if (string.IsNullOrEmpty(text)) { return string.Empty; } bool flag = (Object)(object)Captionman.Instance == (Object)null || !Captionman.Instance.DisableTextColour.Value; StringBuilder stringBuilder = new StringBuilder(text.Length + 16); Stack stack = new Stack(); int num = 0; while (num < text.Length) { if (num + 3 < text.Length && text[num] == '<' && (text[num + 1] == 'c' || text[num + 1] == 'C') && text[num + 2] == ':') { int num2 = text.IndexOf('>', num + 3); if (num2 > num) { string colorToken = text.Substring(num + 3, num2 - (num + 3)).Trim(); if (flag && TryResolveColorToken(colorToken, out string colorHex)) { stringBuilder.Append("'); stack.Push(item: true); } else { stack.Push(item: false); } num = num2 + 1; continue; } } if (num + 3 < text.Length && text[num] == '<' && text[num + 1] == '/' && (text[num + 2] == 'c' || text[num + 2] == 'C') && text[num + 3] == '>') { if (stack.Count > 0 && stack.Pop()) { stringBuilder.Append(""); } num += 4; } else { stringBuilder.Append(text[num]); num++; } } while (stack.Count > 0) { if (stack.Pop()) { stringBuilder.Append(""); } } return stringBuilder.ToString(); } private static bool TryResolveColorToken(string colorToken, out string colorHex) { colorHex = string.Empty; if (ApprovedTextColors.TryGetValue(colorToken, out string value)) { colorHex = value; return true; } if (TryParseRgbColorToken(colorToken, out colorHex)) { return true; } return false; } private static bool TryParseRgbColorToken(string colorToken, out string colorHex) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) colorHex = string.Empty; string[] array = colorToken.Split(',', StringSplitOptions.RemoveEmptyEntries); if (array.Length != 3) { return false; } if (!TryParseColorComponent(array[0], out var component) || !TryParseColorComponent(array[1], out var component2) || !TryParseColorComponent(array[2], out var component3)) { return false; } Color32 val = default(Color32); ((Color32)(ref val))..ctor((byte)component, (byte)component2, (byte)component3, byte.MaxValue); colorHex = "#" + ColorUtility.ToHtmlStringRGB(Color32.op_Implicit(val)); return true; } private static bool TryParseColorComponent(string raw, out int component) { component = 0; string text = raw.Trim(); if (text.Length == 0) { return false; } if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { component = Mathf.Clamp(result, 0, 255); return true; } if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { return false; } if (result2 <= 1f) { component = Mathf.Clamp(Mathf.RoundToInt(result2 * 255f), 0, 255); return true; } component = Mathf.Clamp(Mathf.RoundToInt(result2), 0, 255); return true; } private void ApplyBackgroundOpacity(bool force = false) { //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_backgroundPanel == (Object)null) && !((Object)(object)Captionman.Instance == (Object)null)) { float num = Mathf.Clamp01(Captionman.Instance.BackgroundOpacity.Value); if (force || !Mathf.Approximately(num, _lastAppliedOpacity)) { ((Graphic)_backgroundPanel).color = new Color(_backgroundBaseColor.r, _backgroundBaseColor.g, _backgroundBaseColor.b, num); _lastAppliedOpacity = num; } } } private void UpdatePanelSize() { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_captionText == (Object)null) && !((Object)(object)_containerRect == (Object)null)) { Vector2 preferredValues = ((TMP_Text)_captionText).GetPreferredValues(((TMP_Text)_captionText).text, 480f, 0f); float num = Mathf.Clamp(preferredValues.x + 20f, 180f, 500f); float num2 = Mathf.Clamp(preferredValues.y + 20f, 38f, 260f); _containerRect.sizeDelta = new Vector2(num, num2); } } private void ApplyFontSize(bool force = false) { if (!((Object)(object)_captionText == (Object)null) && !((Object)(object)Captionman.Instance == (Object)null)) { float num = Mathf.Clamp(Captionman.Instance.TextSize.Value, 10f, 25f); if (force || !Mathf.Approximately(num, _lastAppliedFontSize)) { ((TMP_Text)_captionText).fontSize = num; _lastAppliedFontSize = num; } } } private void ApplyTextAlignment(bool force = false) { if (!((Object)(object)_captionText == (Object)null) && !((Object)(object)Captionman.Instance == (Object)null)) { bool value = Captionman.Instance.TextLeftAlign.Value; if (force || !_lastAppliedTextLeftAlign.HasValue || _lastAppliedTextLeftAlign.Value != value) { ((TMP_Text)_captionText).alignment = (TextAlignmentOptions)(value ? 1025 : 1026); _lastAppliedTextLeftAlign = value; } } } private void ApplyContainerPosition(bool force = false) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_containerRect == (Object)null) && !((Object)(object)Captionman.Instance == (Object)null)) { float num = Mathf.Clamp(Captionman.Instance.HorizontalPosition.Value, -270f, 260f); float num2 = Mathf.Clamp(Captionman.Instance.VerticalPosition.Value, 0f, 350f); if (!float.IsFinite(num)) { num = 0f; } if (!float.IsFinite(num2)) { num2 = 50f; } if (force || !Mathf.Approximately(num, _lastAppliedHorizontalPosition) || !Mathf.Approximately(num2, _lastAppliedVerticalPosition)) { _containerRect.anchoredPosition = new Vector2(num, num2); _lastAppliedHorizontalPosition = num; _lastAppliedVerticalPosition = num2; } } } public void ClearCaptions() { _captionQueue.Clear(); if ((Object)(object)_captionText != (Object)null) { ((TMP_Text)_captionText).text = string.Empty; } } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } } internal class GameAudioCaptionService { private readonly Captionman _plugin; private const float ProximityRadius = 30f; private readonly Dictionary _cooldowns = new Dictionary(); public GameAudioCaptionService(Captionman plugin) { _plugin = plugin; } internal void OnAudioEvent(string captionText, Vector3? emitterPosition, bool isGlobal = false) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (_plugin.EnableCaptionsUI.Value && _plugin.GameAudioCaptions.Value) { if (!isGlobal && emitterPosition.HasValue && !IsWithinProximity(emitterPosition.Value)) { Captionman.LogDebug("GameAudio suppressed (out of range): " + captionText); } else if (!IsOnCooldown(captionText)) { SetCooldown(captionText); CaptionUI.AddGameAudioCaptionSafe(captionText); Captionman.LogDebug("GameAudio caption: " + captionText); } } } private bool IsWithinProximity(Vector3 emitterPosition) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) try { PlayerAvatar instance = PlayerAvatar.instance; if ((Object)(object)instance != (Object)null) { return Vector3.Distance(((Component)instance).transform.position, emitterPosition) <= 30f; } Camera main = Camera.main; if ((Object)(object)main != (Object)null) { return Vector3.Distance(((Component)main).transform.position, emitterPosition) <= 30f; } } catch (Exception ex) { Captionman.LogDebug("Proximity check error: " + ex.Message); } return true; } private bool IsOnCooldown(string captionText) { if (!_cooldowns.TryGetValue(captionText, out var value)) { return false; } float num = Mathf.Max(0f, _plugin.GameAudioRepeatCooldownSeconds.Value); return Time.time - value < num; } private void SetCooldown(string captionText) { _cooldowns[captionText] = Time.time; } } [HarmonyPatch] internal static class GameAudioPatches { private static void HandlePlayPostfix(Sound __instance, Vector3 position, AudioSource __result) { //IL_0096: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__result == (Object)null || __instance.Sounds == null || __instance.Sounds.Length == 0) { return; } GameAudioCaptionService gameAudioCaptionService = Captionman.Instance?.GameAudioService; if (gameAudioCaptionService != null) { AudioClip clip = __result.clip; string text = ((clip != null) ? ((Object)clip).name : null); if (string.IsNullOrWhiteSpace(text)) { AudioClip obj = __instance.Sounds[0]; text = ((obj != null) ? ((Object)obj).name : null); } if (!string.IsNullOrWhiteSpace(text) && SoundCaptionCatalog.Current.TryResolve(text, __instance, out string caption, out bool isGlobal)) { Captionman.LogDebug($"GameAudio CSV match: '{text}' -> '{caption}' (global={isGlobal})"); gameAudioCaptionService.OnAudioEvent(caption, position, isGlobal); } } } [HarmonyPostfix] [HarmonyPatch(typeof(Sound), "Play", new Type[] { typeof(Vector3), typeof(float), typeof(float), typeof(float), typeof(float) })] private static void Sound_Play_Vector3_Postfix(Sound __instance, Vector3 position, AudioSource __result) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) HandlePlayPostfix(__instance, position, __result); } [HarmonyPostfix] [HarmonyPatch(typeof(Sound), "Play", new Type[] { typeof(Transform), typeof(float), typeof(float), typeof(float), typeof(float) })] private static void Sound_Play_Transform_Postfix(Sound __instance, Transform followTarget, AudioSource __result) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) HandlePlayPostfix(__instance, ((Object)(object)followTarget != (Object)null) ? followTarget.position : Vector3.zero, __result); } [HarmonyPostfix] [HarmonyPatch(typeof(Sound), "Play", new Type[] { typeof(Transform), typeof(Vector3), typeof(float), typeof(float), typeof(float), typeof(float) })] private static void Sound_Play_TransformContact_Postfix(Sound __instance, Vector3 contactPoint, AudioSource __result) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) HandlePlayPostfix(__instance, contactPoint, __result); } [HarmonyPrefix] [HarmonyPatch(typeof(Sound), "PlayLoop")] private static void Sound_PlayLoop_Prefix(Sound __instance, bool playing) { //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) if (!playing || (Object)(object)__instance.Source == (Object)null || !((Behaviour)__instance.Source).enabled || __instance.Sounds == null || __instance.Sounds.Length == 0) { return; } GameAudioCaptionService gameAudioCaptionService = Captionman.Instance?.GameAudioService; if (gameAudioCaptionService == null) { return; } AudioClip clip = __instance.Source.clip; string text = ((clip != null) ? ((Object)clip).name : null); if (string.IsNullOrWhiteSpace(text)) { AudioClip obj = __instance.Sounds[0]; text = ((obj != null) ? ((Object)obj).name : null); } if (!string.IsNullOrWhiteSpace(text)) { if (!SoundCaptionCatalog.Current.TryResolve(text, __instance, out string caption, out bool isGlobal)) { Captionman.LogDebug("No caption for loop \"" + text + "\""); return; } Vector3 position = ((Component)__instance.Source).transform.position; Captionman.LogDebug($"GameAudio CSV match (loop): '{text}' -> '{caption}' (global={isGlobal})"); gameAudioCaptionService.OnAudioEvent(caption, position, isGlobal); } } } internal sealed class SoundCaptionCatalog { internal readonly struct Entry { internal string Caption { get; } internal bool IsGlobal { get; } internal Entry(string caption, bool isGlobal) { Caption = caption; IsGlobal = isGlobal; } } private sealed class ResolvedCaptionCatalog { internal string CsvPath { get; } internal string Source { get; } internal ResolvedCaptionCatalog(string csvPath, string source) { CsvPath = csvPath; Source = source; } } private static readonly object CatalogLock = new object(); private const string DefaultCaptionFileName = "captionsEN.csv"; private readonly Dictionary _entriesByName; private static SoundCaptionCatalog _current = new SoundCaptionCatalog(new Dictionary(StringComparer.OrdinalIgnoreCase)); internal static SoundCaptionCatalog Current { get { lock (CatalogLock) { return _current; } } } private SoundCaptionCatalog(Dictionary entriesByName) { _entriesByName = entriesByName; } internal static void ReloadFromConfig() { string text = NormalizeCaptionSelector(Captionman.Instance?.GameAudioCaptionFile?.Value); ResolvedCaptionCatalog resolvedCaptionCatalog = ResolveCsvPath(text); string text2 = (string.IsNullOrWhiteSpace(text) ? "captionsEN.csv" : EnsureCsvExtension(text)); if (resolvedCaptionCatalog != null) { string fileName = Path.GetFileName(resolvedCaptionCatalog.CsvPath); if (!string.Equals(text2, fileName, StringComparison.OrdinalIgnoreCase) && string.Equals(fileName, "captionsEN.csv", StringComparison.OrdinalIgnoreCase)) { Captionman.LogWarning("Failed to load " + text2 + ", falling back to captionsEN.csv"); } else { Captionman.LogInfo("Successfully loaded " + fileName); } } SoundCaptionCatalog current = Load(resolvedCaptionCatalog); lock (CatalogLock) { _current = current; } } private static SoundCaptionCatalog Load(ResolvedCaptionCatalog? resolvedCatalog) { if (resolvedCatalog == null) { Captionman.LogWarning("Caption CSV not found. Configure Captions.GameAudioCaptionFile with a CSV filename like captionsEN.csv. captionsEN.csv is always used as fallback when available."); return new SoundCaptionCatalog(new Dictionary(StringComparer.OrdinalIgnoreCase)); } try { string csvPath = resolvedCatalog.CsvPath; List list = File.ReadLines(csvPath).ToList(); if (list.Count == 0) { Captionman.LogWarning("Caption CSV is empty: " + csvPath); return new SoundCaptionCatalog(new Dictionary(StringComparer.OrdinalIgnoreCase)); } List header = ParseCsvLine(list[0]); int num = FindColumnIndex(header, "name"); int num2 = FindColumnIndex(header, "caption"); int num3 = FindColumnIndex(header, "isglobal"); if (num < 0 || num2 < 0) { Captionman.LogError("Caption CSV is missing required columns: name, caption"); return new SoundCaptionCatalog(new Dictionary(StringComparer.OrdinalIgnoreCase)); } Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); int num4 = 0; int num5 = 0; for (int i = 1; i < list.Count; i++) { string text = list[i]; if (string.IsNullOrWhiteSpace(text)) { continue; } List list2 = ParseCsvLine(text); if (list2.Count <= Math.Max(num, num2)) { continue; } string text2 = list2[num].Trim(); string text3 = list2[num2].Trim(); bool flag = num3 >= 0 && num3 < list2.Count && ParseBool(list2[num3]); if (!string.IsNullOrWhiteSpace(text2) && !string.IsNullOrWhiteSpace(text3)) { dictionary[text2] = new Entry(text3, flag); num4++; if (flag) { num5++; } } } Captionman.LogInfo($"Loaded sound caption catalog: {num4} entries ({num5} global) from {Path.GetFileName(csvPath)} ({resolvedCatalog.Source})"); return new SoundCaptionCatalog(dictionary); } catch (Exception ex) { Captionman.LogError("Failed to load sound caption CSV: " + ex.Message); return new SoundCaptionCatalog(new Dictionary(StringComparer.OrdinalIgnoreCase)); } } internal bool TryResolve(string clipName, Sound sound, out string caption, out bool isGlobal) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Invalid comparison between Unknown and I4 caption = string.Empty; isGlobal = false; if (string.IsNullOrWhiteSpace(clipName)) { return false; } if (!_entriesByName.TryGetValue(clipName, out var value)) { Captionman.LogDebug("No caption for \"" + clipName + "\""); return false; } caption = value.Caption; isGlobal = value.IsGlobal || clipName.IndexOf(" global", StringComparison.OrdinalIgnoreCase) >= 0 || (int)sound.Type == 7; return true; } private static ResolvedCaptionCatalog? ResolveCsvPath(string selector) { string text = (string.IsNullOrWhiteSpace(selector) ? "captionsEN.csv" : EnsureCsvExtension(selector)); List searchDirectories = GetSearchDirectories(); foreach (string item in searchDirectories) { string text2 = Path.Combine(item, text); if (File.Exists(text2)) { return new ResolvedCaptionCatalog(text2, "requested-root"); } string text3 = Path.Combine(item, "Captions", text); if (File.Exists(text3)) { return new ResolvedCaptionCatalog(text3, "requested-captions"); } } if (!string.Equals(text, "captionsEN.csv", StringComparison.OrdinalIgnoreCase)) { foreach (string item2 in searchDirectories) { string text4 = Path.Combine(item2, "captionsEN.csv"); if (File.Exists(text4)) { return new ResolvedCaptionCatalog(text4, "default-root"); } string text5 = Path.Combine(item2, "Captions", "captionsEN.csv"); if (File.Exists(text5)) { return new ResolvedCaptionCatalog(text5, "default-captions"); } } } string[] array = new string[3] { "game_audio_captions.csv", "sound_caption_review.csv", "sound_captions.csv" }; foreach (string item3 in searchDirectories) { string[] array2 = array; foreach (string text6 in array2) { string text7 = Path.Combine(item3, "Captions", text6); if (File.Exists(text7)) { Captionman.LogDebug("Using legacy caption catalog fallback: " + text7); return new ResolvedCaptionCatalog(text7, "legacy-captions"); } string text8 = Path.Combine(item3, text6); if (File.Exists(text8)) { Captionman.LogDebug("Using legacy caption catalog fallback: " + text8); return new ResolvedCaptionCatalog(text8, "legacy-root"); } } } return null; } private static string EnsureCsvExtension(string fileName) { if (fileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) { return fileName; } return fileName + ".csv"; } private static string NormalizeCaptionSelector(string? value) { return value?.Trim() ?? string.Empty; } private static List GetSearchDirectories() { List list = new List { Paths.PluginPath, Path.Combine(Paths.PluginPath, "BatteryDie.Captionman"), Path.Combine(Paths.PluginPath, "BatteryDie-Captionman"), Paths.GameRootPath, Paths.ConfigPath }; string directoryName = Path.GetDirectoryName(typeof(Captionman).Assembly.Location); if (!string.IsNullOrWhiteSpace(directoryName)) { list.Insert(0, directoryName); } return list.Where((string dir) => !string.IsNullOrWhiteSpace(dir)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } private static int FindColumnIndex(IReadOnlyList header, string columnName) { for (int i = 0; i < header.Count; i++) { if (string.Equals(header[i].Trim(), columnName, StringComparison.OrdinalIgnoreCase)) { return i; } } return -1; } private static List ParseCsvLine(string line) { List list = new List(); StringBuilder stringBuilder = new StringBuilder(); bool flag = false; for (int i = 0; i < line.Length; i++) { char c = line[i]; switch (c) { case '"': if (flag && i + 1 < line.Length && line[i + 1] == '"') { stringBuilder.Append('"'); i++; } else { flag = !flag; } continue; case ',': if (!flag) { list.Add(stringBuilder.ToString()); stringBuilder.Clear(); continue; } break; } stringBuilder.Append(c); } list.Add(stringBuilder.ToString()); return list; } private static bool ParseBool(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } string text = value.Trim(); if (bool.TryParse(text, out var result)) { return result; } if (!string.Equals(text, "1", StringComparison.OrdinalIgnoreCase) && !string.Equals(text, "yes", StringComparison.OrdinalIgnoreCase)) { return string.Equals(text, "y", StringComparison.OrdinalIgnoreCase); } return true; } } }