Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Captionman v1.0.0
plugins/BatteryDie-Captionman/Captionman.dll
Decompiled 3 hours agousing 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<bool> EnableCaptionsUI { get; private set; } internal ConfigEntry<bool> GameAudioCaptions { get; private set; } internal ConfigEntry<float> GameAudioRepeatCooldownSeconds { get; private set; } internal ConfigEntry<string> GameAudioCaptionFile { get; private set; } internal ConfigEntry<float> BackgroundOpacity { get; private set; } internal ConfigEntry<float> TextSize { get; private set; } internal ConfigEntry<bool> DisableTextColour { get; private set; } internal ConfigEntry<bool> TextLeftAlign { get; private set; } internal ConfigEntry<float> HorizontalPosition { get; private set; } internal ConfigEntry<float> VerticalPosition { get; private set; } internal ConfigEntry<bool> 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<bool>("Captions", "EnableCaptionsUI", true, "Master toggle for caption rendering across the entire game"); GameAudioCaptions = ((BaseUnityPlugin)this).Config.Bind<bool>("Captions", "GameAudioCaptions", true, "Enable closed captions for game audio"); GameAudioRepeatCooldownSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Captions", "GameAudioRepeatCooldownSeconds", 4f, new ConfigDescription("Minimum cooldown in seconds before the same game-audio caption text can appear again", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>())); GameAudioCaptionFile = ((BaseUnityPlugin)this).Config.Bind<string>("Captions", "GameAudioCaptionFile", "captionsEN.csv", "Caption CSV filename to load. If not found, captionsEN.csv is used as fallback."); BackgroundOpacity = ((BaseUnityPlugin)this).Config.Bind<float>("Appearance", "BackgroundOpacity", 0.7f, new ConfigDescription("Opacity of caption background from 0.0 (transparent) to 1.0 (opaque)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); if (BackgroundOpacity.Value < 0f || BackgroundOpacity.Value > 1f) { BackgroundOpacity.Value = Mathf.Clamp01(BackgroundOpacity.Value); ((BaseUnityPlugin)this).Config.Save(); } TextSize = ((BaseUnityPlugin)this).Config.Bind<float>("Appearance", "TextSize", 16f, new ConfigDescription("Caption font size from 10.0 to 25.0", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 25f), Array.Empty<object>())); if (TextSize.Value < 10f || TextSize.Value > 25f) { TextSize.Value = Mathf.Clamp(TextSize.Value, 10f, 25f); ((BaseUnityPlugin)this).Config.Save(); } DisableTextColour = ((BaseUnityPlugin)this).Config.Bind<bool>("Appearance", "DisableTextColour", false, "Disable custom text colour tags (for example <c:red>Alert</c>)"); TextLeftAlign = ((BaseUnityPlugin)this).Config.Bind<bool>("Appearance", "TextLeftAlign", false, "Align caption text to the left instead of centered"); HorizontalPosition = ((BaseUnityPlugin)this).Config.Bind<float>("Appearance", "HorizontalPosition", 0f, new ConfigDescription("Horizontal position offset for captions", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-270f, 260f), Array.Empty<object>())); if (HorizontalPosition.Value < -270f || HorizontalPosition.Value > 260f) { HorizontalPosition.Value = Mathf.Clamp(HorizontalPosition.Value, -270f, 260f); ((BaseUnityPlugin)this).Config.Save(); } VerticalPosition = ((BaseUnityPlugin)this).Config.Bind<float>("Appearance", "VerticalPosition", 50f, new ConfigDescription("Vertical position offset for captions", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 350f), Array.Empty<object>())); if (VerticalPosition.Value < 0f || VerticalPosition.Value > 350f) { VerticalPosition.Value = Mathf.Clamp(VerticalPosition.Value, 0f, 350f); ((BaseUnityPlugin)this).Config.Save(); } EnableDebug = ((BaseUnityPlugin)this).Config.Bind<bool>("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<PendingCaption> PendingCaptions = new Queue<PendingCaption>(); private const int MaxPendingCaptions = 40; private readonly Queue<CaptionEntry> _captionQueue = new Queue<CaptionEntry>(); 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("<c:[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ColorCloseTagRegex = new Regex("</c>", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Dictionary<string, string> ApprovedTextColors = new Dictionary<string, string>(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<CaptionUI>(); if ((Object)(object)captionUI != (Object)null) { Instance = captionUI; return; } GameObject val = new GameObject("CaptionUI"); val.AddComponent<CaptionUI>(); } 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<Canvas>(); val2.renderMode = (RenderMode)0; val2.sortingOrder = 2000; val.AddComponent<CanvasScaler>(); val.AddComponent<GraphicRaycaster>(); return val.GetComponent<RectTransform>(); } 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<RectTransform>(); ((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>(); _canvasGroup.alpha = 1f; GameObject val = new GameObject("CaptionPanel"); RectTransform val2 = val.AddComponent<RectTransform>(); ((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<Image>(); ApplyBackgroundOpacity(force: true); GameObject val3 = new GameObject("CaptionText"); RectTransform val4 = val3.AddComponent<RectTransform>(); ((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<TextMeshProUGUI>(); ((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<TMP_Text>(); 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<string> list = new List<string>(_captionQueue.Count); foreach (CaptionEntry item in _captionQueue) { switch (item.Kind) { case CaptionKind.GameAudio: list.Add(TransformColorTags(item.Text)); break; case CaptionKind.Speaker: list.Add("<b>" + TransformColorTags(item.Speaker) + ":</b> " + 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<bool> stack = new Stack<bool>(); 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("<color=").Append(colorHex).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("</color>"); } num += 4; } else { stringBuilder.Append(text[num]); num++; } } while (stack.Count > 0) { if (stack.Pop()) { stringBuilder.Append("</color>"); } } 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<string, float> _cooldowns = new Dictionary<string, float>(); 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<string, Entry> _entriesByName; private static SoundCaptionCatalog _current = new SoundCaptionCatalog(new Dictionary<string, Entry>(StringComparer.OrdinalIgnoreCase)); internal static SoundCaptionCatalog Current { get { lock (CatalogLock) { return _current; } } } private SoundCaptionCatalog(Dictionary<string, Entry> 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<string, Entry>(StringComparer.OrdinalIgnoreCase)); } try { string csvPath = resolvedCatalog.CsvPath; List<string> list = File.ReadLines(csvPath).ToList(); if (list.Count == 0) { Captionman.LogWarning("Caption CSV is empty: " + csvPath); return new SoundCaptionCatalog(new Dictionary<string, Entry>(StringComparer.OrdinalIgnoreCase)); } List<string> 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<string, Entry>(StringComparer.OrdinalIgnoreCase)); } Dictionary<string, Entry> dictionary = new Dictionary<string, Entry>(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<string> 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<string, Entry>(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<string> 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<string> GetSearchDirectories() { List<string> list = new List<string> { 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<string>(StringComparer.OrdinalIgnoreCase).ToList(); } private static int FindColumnIndex(IReadOnlyList<string> 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<string> ParseCsvLine(string line) { List<string> list = new List<string>(); 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; } } }