Decompiled source of ServerAnnouncements v1.1.0
plugins/ServerAnnouncements/ServerAnnouncements.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CodeTalker.Networking; using CodeTalker.Packets; using HarmonyLib; using Mirror; using ServerAnnouncements.Channels; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("atl_host")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("atl_host")] [assembly: AssemblyCopyright("Copyright \ufffd\ufffd 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("45a14b12-0a67-4261-85f1-09da9e90923b")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace ServerAnnouncements { public static class AnnouncementCommandHandler { [HarmonyPatch(typeof(ChatBehaviour), "Cmd_SendChatMessage")] private static class ChatCommandPatch { [HarmonyPrefix] private static bool Prefix(ChatBehaviour __instance, string _message) { if (string.IsNullOrEmpty(_message) || !_message.StartsWith("/ntc")) { return true; } try { ProcessCommand(__instance, _message); } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogError((object)("Command processing error: " + ex.Message)); LocalMessage(__instance, "<color=#FF6666>[Notice] Command failed. Check log.</color>"); } return false; } } private const string CMD_PREFIX = "/ntc"; public static void Initialize() { ServerAnnouncementsPlugin.Log.LogDebug((object)"Notice command handler initialized."); } private static void ProcessCommand(ChatBehaviour chat, string input) { string[] array = input.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length < 2) { ShowHelp(chat); return; } string text = array[1].ToLower(); bool isHost = AnnouncementManager.IsHost(); switch (text) { case "help": ShowHelp(chat); break; case "toast": if (RequireHost(chat, isHost)) { float num = AnnouncementConfig.ToastDuration.Value; string text2; if (array.Length <= 3 || !float.TryParse(array[2], out var result)) { text2 = ((array.Length <= 2) ? AnnouncementConfig.JoinToastMessage.Value : string.Join(" ", array.Skip(2))); } else { num = result; text2 = string.Join(" ", array.Skip(3)); } AnnouncementNetworkHandler.BroadcastToast(text2, num, skipLocalDisplay: false, forceShow: true); LocalMessage(chat, $"<color=#FFD700>[Notice]</color> Toast sent ({num}s): {text2}"); } break; case "banner": if (RequireHost(chat, isHost)) { ShowBannerToAll(); LocalMessage(chat, "<color=#FFD700>[Notice]</color> Banner shown to all players."); } break; case "version": if (RequireHost(chat, isHost)) { if (array.Length > 2 && int.TryParse(array[2], out var result3)) { AnnouncementConfig.NoticeVersion.Value = result3; LocalMessage(chat, $"<color=#FFD700>[Notice]</color> Notice version set to {result3}"); } else { LocalMessage(chat, $"<color=#FFD700>[Notice]</color> Current version: {AnnouncementConfig.NoticeVersion.Value}"); } } break; case "duration": if (RequireHost(chat, isHost)) { if (array.Length > 2 && float.TryParse(array[2], out var result2)) { result2 = Mathf.Clamp(result2, 1f, 30f); AnnouncementConfig.ToastDuration.Value = result2; LocalMessage(chat, $"<color=#FFD700>[Notice]</color> Toast duration set to {result2}s"); } else { LocalMessage(chat, $"<color=#FFD700>[Notice]</color> Current duration: {AnnouncementConfig.ToastDuration.Value}s (1-30)"); } } break; case "title": if (RequireHost(chat, isHost)) { if (array.Length > 2) { string text5 = string.Join(" ", array.Skip(2)); AnnouncementConfig.BannerTitle.Value = text5; LocalMessage(chat, "<color=#FFD700>[Notice]</color> Banner title set to: " + text5); } else { LocalMessage(chat, "<color=#FFD700>[Notice]</color> Current title: " + AnnouncementConfig.BannerTitle.Value); } } break; case "content": if (RequireHost(chat, isHost)) { if (array.Length > 2) { string value = string.Join(" ", array.Skip(2)); AnnouncementConfig.BannerContent.Value = value; LocalMessage(chat, "<color=#FFD700>[Notice]</color> Banner content set."); } else { LocalMessage(chat, "<color=#FFD700>[Notice]</color> Current content: " + AnnouncementConfig.BannerContent.Value); } } break; case "joinmsg": if (RequireHost(chat, isHost)) { if (array.Length > 2) { string text4 = string.Join(" ", array.Skip(2)); AnnouncementConfig.JoinToastMessage.Value = text4; LocalMessage(chat, "<color=#FFD700>[Notice]</color> Join message set to: " + text4); } else { LocalMessage(chat, "<color=#FFD700>[Notice]</color> Current join message: " + AnnouncementConfig.JoinToastMessage.Value); } } break; case "tab": if (RequireHost(chat, isHost)) { if (array.Length > 2) { string text3 = string.Join(" ", array.Skip(2)); AnnouncementConfig.EasySettingsTab.Value = text3; LocalMessage(chat, "<color=#FFD700>[Notice]</color> EasySettings tab set to: " + text3); LocalMessage(chat, "<color=#AAAAAA>Restart game or reload settings for changes to take effect.</color>"); } else { LocalMessage(chat, "<color=#FFD700>[Notice]</color> Current tab: " + AnnouncementConfig.EasySettingsTab.Value); LocalMessage(chat, "<color=#AAAAAA>Examples: Moderation, Mods, Server</color>"); } } break; case "reload": if (RequireHost(chat, isHost)) { AnnouncementConfig.Reload(); LocalMessage(chat, "<color=#FFD700>[Notice]</color> Config reloaded."); } break; case "status": if (RequireHost(chat, isHost)) { ShowStatus(chat); } break; case "reset": AnnouncementManager.Instance.ResetAllAcknowledgments(); LocalMessage(chat, "<color=#FFD700>[Notice]</color> Notice acknowledgment history cleared."); break; default: LocalMessage(chat, "<color=#FF6666>[Notice]</color> Unknown command: " + text + ". Use /ntc help"); break; } } private static bool RequireHost(ChatBehaviour chat, bool isHost) { if (!isHost) { LocalMessage(chat, "<color=#FF6666>[Notice]</color> This command is host-only."); return false; } return true; } private static void LocalMessage(ChatBehaviour chat, string msg) { chat.New_ChatMessage(msg); } private static void ShowHelp(ChatBehaviour chat) { LocalMessage(chat, "<color=#FFD700>=== Server Notice Commands ===</color>"); LocalMessage(chat, "/ntc help - Show this help"); LocalMessage(chat, "/ntc toast [sec] [msg] - Send toast (Host)"); LocalMessage(chat, "/ntc banner - Show banner to all (Host)"); LocalMessage(chat, "/ntc duration [n] - Set toast duration (Host)"); LocalMessage(chat, "/ntc title [text] - Set banner title (Host)"); LocalMessage(chat, "/ntc content [text] - Set banner content (Host)"); LocalMessage(chat, "/ntc joinmsg [text] - Set join message (Host)"); LocalMessage(chat, "/ntc version [n] - Set notice version (Host)"); LocalMessage(chat, "/ntc tab [name] - Set EasySettings tab (Host)"); LocalMessage(chat, "/ntc status - Show status (Host)"); LocalMessage(chat, "/ntc reload - Reload config (Host)"); LocalMessage(chat, "/ntc reset - Reset your acknowledgments"); } private static void ShowStatus(ChatBehaviour chat) { LocalMessage(chat, "<color=#FFD700>=== Notice Status ===</color>"); LocalMessage(chat, $"Enabled: {AnnouncementConfig.EnableNoticeSystem.Value}"); LocalMessage(chat, $"Version: {AnnouncementConfig.NoticeVersion.Value}"); LocalMessage(chat, "Title: " + AnnouncementConfig.BannerTitle.Value); LocalMessage(chat, $"Join Toast: {AnnouncementConfig.ShowToastOnJoin.Value}"); LocalMessage(chat, $"Toast Duration: {AnnouncementConfig.ToastDuration.Value}s"); LocalMessage(chat, "Join Message: " + AnnouncementConfig.JoinToastMessage.Value); LocalMessage(chat, $"Vanilla Compat: {AnnouncementConfig.VanillaCompatMode.Value}"); } public static void SendToastNow(string message = null) { if (!AnnouncementManager.IsHost()) { ServerAnnouncementsPlugin.Log.LogWarning((object)"SendToastNow called but not host."); return; } string message2 = message ?? AnnouncementConfig.JoinToastMessage.Value; float value = AnnouncementConfig.ToastDuration.Value; AnnouncementNetworkHandler.BroadcastToast(message2, value, skipLocalDisplay: false, forceShow: true); } public static void ShowBannerToAll() { if (!AnnouncementManager.IsHost()) { ServerAnnouncementsPlugin.Log.LogWarning((object)"ShowBannerToAll called but not host."); } else { AnnouncementNetworkHandler.BroadcastBanner(forceShow: true); } } public static void SyncSettingsIfHost() { if (AnnouncementManager.IsHost()) { } } } [Serializable] public class ModeratorSharedContent { public string ToastMessage = "Moderator Announcement"; public float ToastDuration = 5f; public int ToastFontSize = 20; public string BannerTitle = "Moderator Notice"; public string BannerHighlight = ""; public string BannerContent = ""; public string BannerExtraContent = ""; public int TitleFontSize = 24; public int HighlightFontSize = 18; public int ContentFontSize = 16; public int ExtraFontSize = 14; public int SoundType = 0; public float SoundVolume = 0.5f; public bool ScheduleEnabled = false; public float ScheduleInterval = 300f; public bool ScheduleToast = true; public bool ScheduleBanner = false; public string LastEditedBy = ""; public long LastEditedTimestamp = 0L; public DateTime LastEditedAt => DateTimeOffset.FromUnixTimeSeconds(LastEditedTimestamp).LocalDateTime; public void SetLastEdited(string steamId) { LastEditedBy = steamId ?? ""; LastEditedTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); } public string Serialize() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(Escape(ToastMessage)); stringBuilder.Append("|"); stringBuilder.Append(ToastDuration.ToString("F2")); stringBuilder.Append("|"); stringBuilder.Append(ToastFontSize); stringBuilder.Append("|"); stringBuilder.Append(Escape(BannerTitle)); stringBuilder.Append("|"); stringBuilder.Append(Escape(BannerHighlight)); stringBuilder.Append("|"); stringBuilder.Append(Escape(BannerContent)); stringBuilder.Append("|"); stringBuilder.Append(Escape(BannerExtraContent)); stringBuilder.Append("|"); stringBuilder.Append(TitleFontSize); stringBuilder.Append("|"); stringBuilder.Append(HighlightFontSize); stringBuilder.Append("|"); stringBuilder.Append(ContentFontSize); stringBuilder.Append("|"); stringBuilder.Append(ExtraFontSize); stringBuilder.Append("|"); stringBuilder.Append(SoundType); stringBuilder.Append("|"); stringBuilder.Append(SoundVolume.ToString("F2")); stringBuilder.Append("|"); stringBuilder.Append(Escape(LastEditedBy)); stringBuilder.Append("|"); stringBuilder.Append(LastEditedTimestamp); stringBuilder.Append("|"); stringBuilder.Append(ScheduleEnabled ? "1" : "0"); stringBuilder.Append("|"); stringBuilder.Append(ScheduleInterval.ToString("F1")); stringBuilder.Append("|"); stringBuilder.Append(ScheduleToast ? "1" : "0"); stringBuilder.Append("|"); stringBuilder.Append(ScheduleBanner ? "1" : "0"); return stringBuilder.ToString(); static string Escape(string s) { return (s ?? "").Replace("|", "\\|").Replace("\n", "\\n"); } } public static ModeratorSharedContent Deserialize(string data) { ModeratorSharedContent moderatorSharedContent = new ModeratorSharedContent(); if (string.IsNullOrEmpty(data)) { return moderatorSharedContent; } try { string[] array = SplitByUnescapedPipe(data); if (array.Length >= 15) { moderatorSharedContent.ToastMessage = Unescape(array[0]); float.TryParse(array[1], out moderatorSharedContent.ToastDuration); int.TryParse(array[2], out moderatorSharedContent.ToastFontSize); moderatorSharedContent.BannerTitle = Unescape(array[3]); moderatorSharedContent.BannerHighlight = Unescape(array[4]); moderatorSharedContent.BannerContent = Unescape(array[5]); moderatorSharedContent.BannerExtraContent = Unescape(array[6]); int.TryParse(array[7], out moderatorSharedContent.TitleFontSize); int.TryParse(array[8], out moderatorSharedContent.HighlightFontSize); int.TryParse(array[9], out moderatorSharedContent.ContentFontSize); int.TryParse(array[10], out moderatorSharedContent.ExtraFontSize); int.TryParse(array[11], out moderatorSharedContent.SoundType); float.TryParse(array[12], out moderatorSharedContent.SoundVolume); moderatorSharedContent.LastEditedBy = Unescape(array[13]); long.TryParse(array[14], out moderatorSharedContent.LastEditedTimestamp); } if (array.Length >= 19) { moderatorSharedContent.ScheduleEnabled = array[15] == "1"; float.TryParse(array[16], out moderatorSharedContent.ScheduleInterval); moderatorSharedContent.ScheduleToast = array[17] == "1"; moderatorSharedContent.ScheduleBanner = array[18] == "1"; } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("[ModeratorSharedContent] Deserialize error: " + ex.Message)); } return moderatorSharedContent; static string Unescape(string s) { return (s ?? "").Replace("\\|", "|").Replace("\\n", "\n"); } } private static string[] SplitByUnescapedPipe(string input) { List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < input.Length; i++) { if (input[i] == '|' && (i == 0 || input[i - 1] != '\\')) { list.Add(stringBuilder.ToString()); stringBuilder.Clear(); } else { stringBuilder.Append(input[i]); } } list.Add(stringBuilder.ToString()); return list.ToArray(); } public void CopyFrom(ModeratorSharedContent other) { if (other != null) { ToastMessage = other.ToastMessage; ToastDuration = other.ToastDuration; ToastFontSize = other.ToastFontSize; BannerTitle = other.BannerTitle; BannerHighlight = other.BannerHighlight; BannerContent = other.BannerContent; BannerExtraContent = other.BannerExtraContent; TitleFontSize = other.TitleFontSize; HighlightFontSize = other.HighlightFontSize; ContentFontSize = other.ContentFontSize; ExtraFontSize = other.ExtraFontSize; SoundType = other.SoundType; SoundVolume = other.SoundVolume; LastEditedBy = other.LastEditedBy; LastEditedTimestamp = other.LastEditedTimestamp; ScheduleEnabled = other.ScheduleEnabled; ScheduleInterval = other.ScheduleInterval; ScheduleToast = other.ScheduleToast; ScheduleBanner = other.ScheduleBanner; } } } public static class ModeratorContentManager { private static ModeratorSharedContent _sharedContent = new ModeratorSharedContent(); private static string _saveFilePath = null; public static ModeratorSharedContent SharedContent => _sharedContent; public static void Initialize() { _saveFilePath = Path.Combine(Paths.ConfigPath, "ServerAnnouncements_ModeratorContent.json"); LoadFromFile(); } public static void OnServerStarted() { if (AnnouncementManager.IsHost() && _sharedContent.ScheduleEnabled) { AnnouncementRouter.Instance.StartSharedScheduler(); } } public static void UpdateContent(ModeratorSharedContent content, string editorSteamId, bool syncToOthers = true) { if (content == null) { return; } _sharedContent.CopyFrom(content); _sharedContent.SetLastEdited(editorSteamId); if (AnnouncementManager.IsHost()) { SaveToFile(); AnnouncementRouter.Instance.UpdateSharedSchedulerState(); if (syncToOthers) { BroadcastContentSync(); } } } public static void UpdateFromSync(string serializedData) { ModeratorSharedContent other = ModeratorSharedContent.Deserialize(serializedData); _sharedContent.CopyFrom(other); } public static void BroadcastContentSync() { if (AnnouncementManager.IsHost()) { string serializedContent = _sharedContent.Serialize(); CodeTalkerIntegration.BroadcastModeratorContentSync(serializedContent); } } public static void SendContentUpdateToHost() { if (AnnouncementManager.IsHost()) { _sharedContent.SetLastEdited(PermissionHelper.GetLocalSteamId()); SaveToFile(); BroadcastContentSync(); } else { string serializedContent = _sharedContent.Serialize(); CodeTalkerIntegration.SendModeratorContentUpdate(serializedContent); } } private static void SaveToFile() { if (string.IsNullOrEmpty(_saveFilePath)) { return; } try { string contents = _sharedContent.Serialize(); File.WriteAllText(_saveFilePath, contents); ServerAnnouncementsPlugin.Log.LogDebug((object)("[ModeratorContent] Saved to " + _saveFilePath)); } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("[ModeratorContent] Save error: " + ex.Message)); } } private static void LoadFromFile() { if (string.IsNullOrEmpty(_saveFilePath) || !File.Exists(_saveFilePath)) { return; } try { string data = File.ReadAllText(_saveFilePath); ModeratorSharedContent moderatorSharedContent = ModeratorSharedContent.Deserialize(data); if (moderatorSharedContent != null) { _sharedContent.CopyFrom(moderatorSharedContent); ServerAnnouncementsPlugin.Log.LogInfo((object)("[ModeratorContent] Loaded from file. Last edited by " + _sharedContent.LastEditedBy)); } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("[ModeratorContent] Load error: " + ex.Message)); } } public static void Clear() { _sharedContent = new ModeratorSharedContent(); } } public static class AnnouncementConfig { public static ConfigEntry<bool> EnableNoticeSystem; public static ConfigEntry<string> BannerTitle; public static ConfigEntry<string> BannerHighlight; public static ConfigEntry<string> BannerContent; public static ConfigEntry<string> BannerExtraContent; public static ConfigEntry<int> NoticeVersion; public static ConfigEntry<bool> ShowToastOnJoin; public static ConfigEntry<bool> ShowBannerOnJoin; public static ConfigEntry<bool> ShowToastOnJoinSelf; public static ConfigEntry<bool> ShowBannerOnJoinSelf; public static ConfigEntry<string> JoinToastMessage; public static ConfigEntry<float> ToastDuration; public static ConfigEntry<bool> VanillaCompatMode; public static ConfigEntry<string> ModeratorSteamIds; public static ConfigEntry<float> BroadcastCooldown; public static ConfigEntry<bool> EnableScheduledAnnouncement; public static ConfigEntry<float> ScheduledAnnouncementInterval; public static ConfigEntry<bool> ScheduledAnnouncementToast; public static ConfigEntry<bool> ScheduledAnnouncementBanner; public static ConfigEntry<bool> ScheduledAnnouncementIncludeHost; public static ConfigEntry<bool> ScheduledAnnouncementPlaySound; public static ConfigEntry<int> ScheduledSoundIndex; public static ConfigEntry<float> ScheduledSoundVolume; public static ConfigEntry<bool> EnableNoticeDisplay; public static ConfigEntry<bool> PlayNoticeSound; public static ConfigEntry<int> NotificationSoundIndex; public static ConfigEntry<float> NotificationSoundVolume; public static ConfigEntry<bool> IgnoreNormalAnnouncementSound; public static ConfigEntry<bool> IgnoreScheduledAnnouncementSound; public static ConfigEntry<string> EasySettingsTab; public static ConfigEntry<KeyCode> SettingsWindowKey; public static ConfigEntry<int> BannerTitleFontSize; public static ConfigEntry<int> BannerHighlightFontSize; public static ConfigEntry<int> BannerContentFontSize; public static ConfigEntry<int> BannerExtraContentFontSize; public static ConfigEntry<int> ToastFontSize; private static ConfigFile _config; public static void Initialize(ConfigFile config) { //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Expected O, but got Unknown //IL_01f4: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Expected O, but got Unknown //IL_0247: Unknown result type (might be due to invalid IL or missing references) //IL_0251: Expected O, but got Unknown //IL_02df: Unknown result type (might be due to invalid IL or missing references) //IL_02e9: Expected O, but got Unknown //IL_0317: Unknown result type (might be due to invalid IL or missing references) //IL_0321: Expected O, but got Unknown //IL_03b0: Unknown result type (might be due to invalid IL or missing references) //IL_03ba: Expected O, but got Unknown //IL_03e8: Unknown result type (might be due to invalid IL or missing references) //IL_03f2: Expected O, but got Unknown //IL_0455: Unknown result type (might be due to invalid IL or missing references) //IL_045f: Expected O, but got Unknown //IL_0484: Unknown result type (might be due to invalid IL or missing references) //IL_048e: Expected O, but got Unknown //IL_04b3: Unknown result type (might be due to invalid IL or missing references) //IL_04bd: Expected O, but got Unknown //IL_04e2: Unknown result type (might be due to invalid IL or missing references) //IL_04ec: Expected O, but got Unknown //IL_0511: Unknown result type (might be due to invalid IL or missing references) //IL_051b: Expected O, but got Unknown _config = config; EnableNoticeSystem = config.Bind<bool>("Host", "EnableNoticeSystem", true, "Enable the server notice system (Host only)"); BannerTitle = config.Bind<string>("Host", "BannerTitle", "Server Rules", "Title displayed on the notice banner"); BannerHighlight = config.Bind<string>("Host", "BannerHighlight", "", "Highlighted content displayed before main content (visible to vanilla clients)"); BannerContent = config.Bind<string>("Host", "BannerContent", "Welcome to the server!\nPlease follow the rules.", "Content displayed on the notice banner (supports \\n for new lines)"); BannerExtraContent = config.Bind<string>("Host", "BannerExtraContent", "", "Additional content displayed below the main banner content (supports \\n for new lines)"); NoticeVersion = config.Bind<int>("Host", "NoticeVersion", 1, new ConfigDescription("Increment to show banner again to all players", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 999), Array.Empty<object>())); ShowToastOnJoin = config.Bind<bool>("Host", "ShowToastOnJoin", true, "Show a toast message when players join the server"); ShowBannerOnJoin = config.Bind<bool>("Host", "ShowBannerOnJoin", true, "Show the notice banner when players join the server"); ShowToastOnJoinSelf = config.Bind<bool>("Host", "ShowToastOnJoinSelf", true, "Show a toast message to the host when hosting starts"); ShowBannerOnJoinSelf = config.Bind<bool>("Host", "ShowBannerOnJoinSelf", true, "Show the notice banner to the host when hosting starts"); JoinToastMessage = config.Bind<string>("Host", "JoinToastMessage", "Welcome to the server!", "Toast message shown when players join"); ToastDuration = config.Bind<float>("Host", "ToastDuration", 5f, new ConfigDescription("How long toast messages display (seconds)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f), Array.Empty<object>())); VanillaCompatMode = config.Bind<bool>("Host", "VanillaCompatMode", true, "Also send notices as regular chat messages for players without the mod"); ModeratorSteamIds = config.Bind<string>("Host", "ModeratorSteamIds", "", "Comma-separated list of Steam IDs that have moderator permissions (can send announcements). Example: 76561198012345678,76561198087654321"); BroadcastCooldown = config.Bind<float>("Host", "BroadcastCooldown", 3f, new ConfigDescription("Minimum seconds between manual broadcasts (prevents spam)", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>())); EnableScheduledAnnouncement = config.Bind<bool>("Host", "EnableScheduledAnnouncement", false, "Enable automatic scheduled announcements"); ScheduledAnnouncementInterval = config.Bind<float>("Host", "ScheduledAnnouncementInterval", 300f, new ConfigDescription("Interval between scheduled announcements (seconds). Min 5s for testing, Max 24 hours.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 86400f), Array.Empty<object>())); ScheduledAnnouncementToast = config.Bind<bool>("Host", "ScheduledAnnouncementToast", true, "Send toast message in scheduled announcements"); ScheduledAnnouncementBanner = config.Bind<bool>("Host", "ScheduledAnnouncementBanner", false, "Send banner in scheduled announcements"); ScheduledAnnouncementIncludeHost = config.Bind<bool>("Host", "ScheduledAnnouncementIncludeHost", false, "Include host in scheduled announcements (if false, only clients receive)"); ScheduledAnnouncementPlaySound = config.Bind<bool>("Host", "ScheduledAnnouncementPlaySound", false, "Play notification sound for scheduled announcements (separate from normal notice sound setting)"); ScheduledSoundIndex = config.Bind<int>("Host", "ScheduledSoundIndex", 0, new ConfigDescription("Sound type for scheduled announcements", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 7), Array.Empty<object>())); ScheduledSoundVolume = config.Bind<float>("Host", "ScheduledSoundVolume", 0.5f, new ConfigDescription("Volume for scheduled announcement sounds", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); EnableNoticeDisplay = config.Bind<bool>("Client", "EnableNoticeDisplay", true, "Show server notices (disable to hide all notices)"); PlayNoticeSound = config.Bind<bool>("Client", "PlayNoticeSound", true, "Play sound when notice appears"); IgnoreNormalAnnouncementSound = config.Bind<bool>("Client", "IgnoreNormalAnnouncementSound", false, "Ignore sounds from normal (non-scheduled) announcements sent by host"); IgnoreScheduledAnnouncementSound = config.Bind<bool>("Client", "IgnoreScheduledAnnouncementSound", false, "Ignore sounds from scheduled announcements sent by host"); NotificationSoundIndex = config.Bind<int>("Client", "NotificationSoundIndex", 0, new ConfigDescription("Index of the sound to play for notifications (use Sound Test in settings to find)", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>())); NotificationSoundVolume = config.Bind<float>("Client", "NotificationSoundVolume", 0.5f, new ConfigDescription("Volume of notification sounds", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); EasySettingsTab = config.Bind<string>("UI", "EasySettingsTab", "Moderation", "Which EasySettings tab to show this mod in (e.g., 'Moderation', 'Mods', or custom name). Empty = default Mods tab."); SettingsWindowKey = config.Bind<KeyCode>("UI", "SettingsWindowKey", (KeyCode)291, "Key to open/close the settings window. Default: F10"); BannerTitleFontSize = config.Bind<int>("UI", "BannerTitleFontSize", 24, new ConfigDescription("Font size for banner title", (AcceptableValueBase)(object)new AcceptableValueRange<int>(12, 48), Array.Empty<object>())); BannerHighlightFontSize = config.Bind<int>("UI", "BannerHighlightFontSize", 18, new ConfigDescription("Font size for banner highlight content", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 36), Array.Empty<object>())); BannerContentFontSize = config.Bind<int>("UI", "BannerContentFontSize", 16, new ConfigDescription("Font size for banner content", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 36), Array.Empty<object>())); BannerExtraContentFontSize = config.Bind<int>("UI", "BannerExtraContentFontSize", 14, new ConfigDescription("Font size for banner extra content", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 36), Array.Empty<object>())); ToastFontSize = config.Bind<int>("UI", "ToastFontSize", 20, new ConfigDescription("Font size for toast messages", (AcceptableValueBase)(object)new AcceptableValueRange<int>(12, 36), Array.Empty<object>())); } public static void Reload() { ConfigFile config = _config; if (config != null) { config.Reload(); } } } public struct DismissedBannerInfo { public int Version; public DateTime DismissedAt; } public class AnnouncementManager { private static AnnouncementManager _instance; private Dictionary<string, DismissedBannerInfo> _dismissedBanners = new Dictionary<string, DismissedBannerInfo>(); private string _currentServerId; private const float DISMISS_DURATION_HOURS = 24f; private bool _receivedBannerThisSession = false; private bool _receivedToastThisSession = false; private bool? _lastBannerSoundOverride = null; private bool? _lastToastSoundOverride = null; private int _lastSoundType = 0; private float _lastSoundVolume = 0.5f; private bool _lastIsFromNetwork = false; private bool _lastIsScheduled = false; public static AnnouncementManager Instance => _instance ?? (_instance = new AnnouncementManager()); public event Action<AnnouncementData> OnBannerReceived; public event Action<string, float, int> OnToastReceived; private AnnouncementManager() { LoadDismissedBanners(); } public void SetCurrentServer(string serverId) { _currentServerId = serverId; } public void ClearCurrentServer() { _currentServerId = null; _receivedBannerThisSession = false; _receivedToastThisSession = false; } public bool IsBannerDismissed(int version) { if (string.IsNullOrEmpty(_currentServerId)) { return false; } if (_dismissedBanners.TryGetValue(_currentServerId, out var value)) { if (value.Version != version) { return false; } if ((DateTime.Now - value.DismissedAt).TotalHours >= 24.0) { return false; } return true; } return false; } public void DismissBanner(int version) { if (!string.IsNullOrEmpty(_currentServerId)) { _dismissedBanners[_currentServerId] = new DismissedBannerInfo { Version = version, DismissedAt = DateTime.Now }; SaveDismissedBanners(); } } public void ResetAllAcknowledgments() { _dismissedBanners.Clear(); SaveDismissedBanners(); } public void HandleBannerReceived(AnnouncementData data, bool forceShow = false, bool? playSound = null, int soundType = -1, float soundVolume = -1f) { if (AnnouncementConfig.EnableNoticeDisplay.Value && (forceShow || !_receivedBannerThisSession) && (forceShow || !IsBannerDismissed(data.Version))) { _lastBannerSoundOverride = playSound; _lastSoundType = ((soundType >= 0) ? soundType : (AnnouncementConfig.NotificationSoundIndex?.Value ?? 0)); _lastSoundVolume = ((soundVolume >= 0f) ? soundVolume : (AnnouncementConfig.NotificationSoundVolume?.Value ?? 0.5f)); _lastIsFromNetwork = soundType >= 0; _lastIsScheduled = false; _receivedBannerThisSession = true; this.OnBannerReceived?.Invoke(data); } } public void HandleToastReceived(string message, float duration, int fontSize = 20, bool forceShow = false, bool? playSound = null, int soundType = -1, float soundVolume = -1f) { if (AnnouncementConfig.EnableNoticeDisplay.Value && (forceShow || !_receivedToastThisSession)) { _lastToastSoundOverride = playSound; _lastSoundType = ((soundType >= 0) ? soundType : (AnnouncementConfig.NotificationSoundIndex?.Value ?? 0)); _lastSoundVolume = ((soundVolume >= 0f) ? soundVolume : (AnnouncementConfig.NotificationSoundVolume?.Value ?? 0.5f)); _lastIsFromNetwork = soundType >= 0; _lastIsScheduled = false; _receivedToastThisSession = true; this.OnToastReceived?.Invoke(message, duration, fontSize); } } public void SetLastIsScheduled(bool isScheduled) { _lastIsScheduled = isScheduled; } public bool ShouldPlayBannerSound() { if (_lastBannerSoundOverride.HasValue) { return _lastBannerSoundOverride.Value; } if (_lastIsFromNetwork && !IsHost()) { if (_lastIsScheduled) { ConfigEntry<bool> ignoreScheduledAnnouncementSound = AnnouncementConfig.IgnoreScheduledAnnouncementSound; if (ignoreScheduledAnnouncementSound != null && ignoreScheduledAnnouncementSound.Value) { return false; } } if (!_lastIsScheduled) { ConfigEntry<bool> ignoreNormalAnnouncementSound = AnnouncementConfig.IgnoreNormalAnnouncementSound; if (ignoreNormalAnnouncementSound != null && ignoreNormalAnnouncementSound.Value) { return false; } } } return AnnouncementConfig.PlayNoticeSound?.Value ?? true; } public bool ShouldPlayToastSound() { if (_lastToastSoundOverride.HasValue) { return _lastToastSoundOverride.Value; } if (_lastIsFromNetwork && !IsHost()) { if (_lastIsScheduled) { ConfigEntry<bool> ignoreScheduledAnnouncementSound = AnnouncementConfig.IgnoreScheduledAnnouncementSound; if (ignoreScheduledAnnouncementSound != null && ignoreScheduledAnnouncementSound.Value) { return false; } } if (!_lastIsScheduled) { ConfigEntry<bool> ignoreNormalAnnouncementSound = AnnouncementConfig.IgnoreNormalAnnouncementSound; if (ignoreNormalAnnouncementSound != null && ignoreNormalAnnouncementSound.Value) { return false; } } } return AnnouncementConfig.PlayNoticeSound?.Value ?? true; } public int GetLastSoundType() { return _lastSoundType; } public float GetLastSoundVolume() { return _lastSoundVolume; } private void LoadDismissedBanners() { try { string @string = PlayerPrefs.GetString("ServerAnnouncements_DismissedBanners", ""); if (string.IsNullOrEmpty(@string)) { return; } string[] array = @string.Split(new char[1] { ';' }); string[] array2 = array; foreach (string text in array2) { string[] array3 = text.Split(new char[1] { '|' }); if (array3.Length == 3 && int.TryParse(array3[1], out var result) && long.TryParse(array3[2], out var result2)) { _dismissedBanners[array3[0]] = new DismissedBannerInfo { Version = result, DismissedAt = new DateTime(result2) }; } } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("Failed to load dismissed banners: " + ex.Message)); } } private void SaveDismissedBanners() { try { List<string> list = new List<string>(); foreach (KeyValuePair<string, DismissedBannerInfo> dismissedBanner in _dismissedBanners) { list.Add($"{dismissedBanner.Key}|{dismissedBanner.Value.Version}|{dismissedBanner.Value.DismissedAt.Ticks}"); } PlayerPrefs.SetString("ServerAnnouncements_DismissedBanners", string.Join(";", list)); PlayerPrefs.Save(); } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("Failed to save dismissed banners: " + ex.Message)); } } public static bool IsHost() { return NetworkServer.active && (Object)(object)Player._mainPlayer != (Object)null && Player._mainPlayer._isHostPlayer; } public static string GetServerIdentifier() { if (NetworkClient.active && NetworkClient.connection != null) { return $"server_{NetworkClient.connection.connectionId}"; } return "local"; } } public struct AnnouncementData { public string Title; public string Highlight; public string Content; public string ExtraContent; public int Version; public string ServerName; public int TitleFontSize; public int HighlightFontSize; public int ContentFontSize; public int ExtraContentFontSize; public AnnouncementData(string title, string content, int version, string serverName = "", int titleFontSize = 24, int contentFontSize = 16, string extraContent = "", int extraContentFontSize = 14, string highlight = "", int highlightFontSize = 18) { Title = title; Highlight = highlight; Content = content; ExtraContent = extraContent; Version = version; ServerName = serverName; TitleFontSize = titleFontSize; HighlightFontSize = highlightFontSize; ContentFontSize = contentFontSize; ExtraContentFontSize = extraContentFontSize; } } public enum PermissionLevel { Client, Moderator, Host } public static class PermissionHelper { private static HashSet<string> _moderatorCache = null; private static string _lastModeratorConfig = null; private static HashSet<string> _syncedModeratorList = new HashSet<string>(); private static bool _hasSyncedList = false; public static bool HasSyncedList => _hasSyncedList; public static PermissionLevel GetCurrentPermission() { if (AnnouncementManager.IsHost()) { return PermissionLevel.Host; } string localSteamId = GetLocalSteamId(); if (!string.IsNullOrEmpty(localSteamId) && IsModerator(localSteamId)) { return PermissionLevel.Moderator; } return PermissionLevel.Client; } public static bool IsModerator(string steamId) { if (string.IsNullOrEmpty(steamId)) { return false; } if (AnnouncementManager.IsHost()) { RefreshCacheIfNeeded(); return _moderatorCache.Contains(steamId.Trim()); } return _syncedModeratorList.Contains(steamId.Trim()); } public static void UpdateSyncedModeratorList(IEnumerable<string> moderatorIds) { _syncedModeratorList.Clear(); if (moderatorIds != null) { foreach (string moderatorId in moderatorIds) { if (!string.IsNullOrEmpty(moderatorId)) { _syncedModeratorList.Add(moderatorId.Trim()); } } } _hasSyncedList = true; } public static void ClearSyncedModeratorList() { _syncedModeratorList.Clear(); _hasSyncedList = false; } public static bool CanSendAnnouncements() { return GetCurrentPermission() >= PermissionLevel.Moderator; } public static bool CanDelegatePermissions() { return GetCurrentPermission() >= PermissionLevel.Host; } public static string GetLocalSteamId() { try { Player mainPlayer = Player._mainPlayer; if ((Object)(object)mainPlayer != (Object)null) { return mainPlayer._steamID ?? ""; } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogDebug((object)("[PermissionHelper] Failed to get local Steam ID: " + ex.Message)); } return ""; } public static List<string> GetModeratorList() { RefreshCacheIfNeeded(); return _moderatorCache.ToList(); } public static bool AddModerator(string steamId) { if (string.IsNullOrEmpty(steamId)) { return false; } steamId = steamId.Trim(); if (!IsValidSteamId(steamId)) { ServerAnnouncementsPlugin.Log.LogWarning((object)("[PermissionHelper] Invalid Steam ID format: " + steamId)); return false; } RefreshCacheIfNeeded(); if (_moderatorCache.Contains(steamId)) { return false; } _moderatorCache.Add(steamId); SaveModeratorList(); ServerAnnouncementsPlugin.Log.LogInfo((object)("[PermissionHelper] Added moderator: " + steamId)); return true; } public static bool RemoveModerator(string steamId) { if (string.IsNullOrEmpty(steamId)) { return false; } steamId = steamId.Trim(); RefreshCacheIfNeeded(); if (!_moderatorCache.Contains(steamId)) { return false; } _moderatorCache.Remove(steamId); SaveModeratorList(); ServerAnnouncementsPlugin.Log.LogInfo((object)("[PermissionHelper] Removed moderator: " + steamId)); return true; } public static bool IsValidSteamId(string steamId) { if (string.IsNullOrEmpty(steamId)) { return false; } steamId = steamId.Trim(); if (steamId.Length != 17) { return false; } if (!steamId.StartsWith("7656")) { return false; } return steamId.All(char.IsDigit); } public static string GetPermissionName(PermissionLevel level) { return level switch { PermissionLevel.Host => "Host", PermissionLevel.Moderator => "Moderator", _ => "Client", }; } private static void RefreshCacheIfNeeded() { string text = AnnouncementConfig.ModeratorSteamIds?.Value ?? ""; if (_moderatorCache != null && !(_lastModeratorConfig != text)) { return; } _moderatorCache = new HashSet<string>(); _lastModeratorConfig = text; if (string.IsNullOrEmpty(text)) { return; } string[] array = text.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); string[] array2 = array; foreach (string text2 in array2) { string text3 = text2.Trim(); if (!string.IsNullOrEmpty(text3)) { _moderatorCache.Add(text3); } } } private static void SaveModeratorList() { if (AnnouncementConfig.ModeratorSteamIds != null) { string text = string.Join(",", _moderatorCache); AnnouncementConfig.ModeratorSteamIds.Value = text; _lastModeratorConfig = text; } BroadcastModeratorSync(); } public static void BroadcastModeratorSync() { if (AnnouncementManager.IsHost()) { RefreshCacheIfNeeded(); string moderatorList = string.Join(",", _moderatorCache); CodeTalkerIntegration.BroadcastPermissionSync(moderatorList); } } public static void ClearCache() { _moderatorCache = null; _lastModeratorConfig = null; _syncedModeratorList.Clear(); _hasSyncedList = false; } } public enum NotificationSoundType { None, Slap, LowHealthWarning, LexiconBell, PartyInvite, TriggerMessage, Lockout, ErrorPrompt } public static class SoundHelper { private static AudioSource _cachedAudioSource; private static Dictionary<NotificationSoundType, AudioClip> _soundClips = new Dictionary<NotificationSoundType, AudioClip>(); private static bool _initialized = false; private static List<NotificationSoundType> _availableSounds = new List<NotificationSoundType>(); private static string[] _availableSoundNames = null; private static readonly Dictionary<NotificationSoundType, string[]> SoundFieldNames = new Dictionary<NotificationSoundType, string[]> { { NotificationSoundType.Slap, new string[1] { "_slap" } }, { NotificationSoundType.LowHealthWarning, new string[1] { "_lowHealthWarning" } }, { NotificationSoundType.LexiconBell, new string[1] { "_lexiconBell" } }, { NotificationSoundType.PartyInvite, new string[1] { "_partyInviteSfx" } }, { NotificationSoundType.TriggerMessage, new string[1] { "_triggerMessageTone" } }, { NotificationSoundType.Lockout, new string[2] { "lockout", "_lockout" } }, { NotificationSoundType.ErrorPrompt, new string[3] { "ui_errorPrompt", "_errorPrompt", "_ui_errorPrompt" } } }; private static readonly Dictionary<NotificationSoundType, string> SoundDisplayNames = new Dictionary<NotificationSoundType, string> { { NotificationSoundType.None, "None (Silent)" }, { NotificationSoundType.Slap, "Slap" }, { NotificationSoundType.LowHealthWarning, "Low Health Warning" }, { NotificationSoundType.LexiconBell, "Lexicon Bell" }, { NotificationSoundType.PartyInvite, "Party Invite" }, { NotificationSoundType.TriggerMessage, "Trigger Message" }, { NotificationSoundType.Lockout, "Lockout" }, { NotificationSoundType.ErrorPrompt, "Error Prompt" } }; public static int AvailableSoundCount => _availableSounds?.Count ?? 1; public static int SoundCount => Enum.GetValues(typeof(NotificationSoundType)).Length; public static List<NotificationSoundType> GetAvailableSounds() { EnsureInitialized(); return _availableSounds; } public static string[] GetAvailableSoundNames() { EnsureInitialized(); if (_availableSoundNames == null) { List<string> list = new List<string>(); foreach (NotificationSoundType availableSound in _availableSounds) { list.Add(SoundDisplayNames.TryGetValue(availableSound, out var value) ? value : availableSound.ToString()); } _availableSoundNames = list.ToArray(); } return _availableSoundNames; } public static NotificationSoundType GetSoundTypeByAvailableIndex(int availableIndex) { EnsureInitialized(); if (availableIndex >= 0 && availableIndex < _availableSounds.Count) { return _availableSounds[availableIndex]; } return NotificationSoundType.None; } public static int GetAvailableIndexForSoundType(NotificationSoundType soundType) { EnsureInitialized(); return _availableSounds.IndexOf(soundType); } public static void PlaySound(NotificationSoundType soundType, float volume = 0.5f) { if (soundType == NotificationSoundType.None) { return; } EnsureInitialized(); if (!_soundClips.TryGetValue(soundType, out var value) || (Object)(object)value == (Object)null) { FindSoundClip(soundType); _soundClips.TryGetValue(soundType, out value); } if ((Object)(object)value == (Object)null) { ServerAnnouncementsPlugin.Log.LogDebug((object)$"[SoundHelper] Sound clip not found for {soundType}"); return; } AudioSource audioSource = GetAudioSource(); if ((Object)(object)audioSource != (Object)null) { audioSource.PlayOneShot(value, volume); } } public static void PlayNotificationSound() { int soundType = AnnouncementConfig.NotificationSoundIndex?.Value ?? 0; float volume = AnnouncementConfig.NotificationSoundVolume?.Value ?? 0.5f; PlaySound((NotificationSoundType)soundType, volume); } public static void PlaySoundByIndex(int index, float volume) { if (index >= 0 && index < SoundCount) { PlaySound((NotificationSoundType)index, volume); } } public static void RefreshSounds() { _initialized = false; _soundClips.Clear(); _availableSounds.Clear(); _availableSoundNames = null; EnsureInitialized(); } private static void EnsureInitialized() { if (_initialized) { return; } _initialized = true; _availableSounds.Clear(); _availableSoundNames = null; _availableSounds.Add(NotificationSoundType.None); try { foreach (NotificationSoundType value in Enum.GetValues(typeof(NotificationSoundType))) { if (value != 0) { FindSoundClip(value); if (_soundClips.ContainsKey(value) && (Object)(object)_soundClips[value] != (Object)null) { _availableSounds.Add(value); } } } ServerAnnouncementsPlugin.Log.LogDebug((object)$"[SoundHelper] Initialized. Found {_soundClips.Count} sounds."); } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("[SoundHelper] Init error: " + ex.Message)); } } private static void FindSoundClip(NotificationSoundType soundType) { if (!SoundFieldNames.TryGetValue(soundType, out var value)) { return; } MonoBehaviour[] array = Object.FindObjectsOfType<MonoBehaviour>(); MonoBehaviour[] array2 = array; foreach (MonoBehaviour val in array2) { Type type = ((object)val).GetType(); string[] array3 = value; foreach (string name in array3) { FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && field.FieldType == typeof(AudioClip)) { object? value2 = field.GetValue(val); AudioClip val2 = (AudioClip)((value2 is AudioClip) ? value2 : null); if ((Object)(object)val2 != (Object)null) { _soundClips[soundType] = val2; return; } } } } } private static AudioSource GetAudioSource() { //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Expected O, but got Unknown if ((Object)(object)_cachedAudioSource != (Object)null && (Object)(object)((Component)_cachedAudioSource).gameObject != (Object)null) { return _cachedAudioSource; } ActionBarManager val = Object.FindObjectOfType<ActionBarManager>(); if ((Object)(object)val != (Object)null) { FieldInfo field = ((object)val).GetType().GetField("_aSrc", BindingFlags.Instance | BindingFlags.NonPublic); if (field != null) { object? value = field.GetValue(val); _cachedAudioSource = (AudioSource)((value is AudioSource) ? value : null); if ((Object)(object)_cachedAudioSource != (Object)null) { return _cachedAudioSource; } } } AudioSource[] array = Object.FindObjectsOfType<AudioSource>(); if (array.Length != 0) { _cachedAudioSource = array[0]; return _cachedAudioSource; } GameObject val2 = new GameObject("ServerNotice_AudioSource"); Object.DontDestroyOnLoad((Object)(object)val2); _cachedAudioSource = val2.AddComponent<AudioSource>(); return _cachedAudioSource; } public static void ClearCache() { _cachedAudioSource = null; _initialized = false; _soundClips.Clear(); _availableSounds.Clear(); _availableSoundNames = null; } } public class AnnouncementRouter { [CompilerGenerated] private sealed class <HandshakeAndNotifyCoroutine>d__33 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public int connectionId; public AnnouncementRouter <>4__this; private PlayerInfo <player>5__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <HandshakeAndNotifyCoroutine>d__33(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <player>5__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(2f); <>1__state = 1; return true; case 1: <>1__state = -1; <player>5__1 = PlayerTypeManager.GetPlayer(connectionId); if (<player>5__1 == null) { return false; } <>4__this._modClientChannel.SendHandshake(connectionId); PlayerTypeManager.MarkHandshakeSent(connectionId); <>2__current = (object)new WaitForSeconds(3f); <>1__state = 2; return true; case 2: <>1__state = -1; PlayerTypeManager.CheckHandshakeTimeouts(); <player>5__1 = PlayerTypeManager.GetPlayer(connectionId); if (<player>5__1 == null) { return false; } if (AnnouncementConfig.EnableNoticeSystem.Value) { <>4__this.SendAnnouncementsToPlayer(connectionId); } return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <SchedulerCoroutine>d__22 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public AnnouncementRouter <>4__this; private float <interval>5__1; private bool <sendToast>5__2; private bool <sendBanner>5__3; private bool <includeHost>5__4; private bool <playSound>5__5; private int <schedSoundType>5__6; private float <schedSoundVolume>5__7; private string <message>5__8; private float <duration>5__9; private int <fontSize>5__10; private AnnouncementData <data>5__11; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SchedulerCoroutine>d__22(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <message>5__8 = null; <data>5__11 = default(AnnouncementData); <>1__state = -2; } private bool MoveNext() { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>4__this._lastScheduledTime = Time.time; break; case 1: { <>1__state = -1; ConfigEntry<bool> enableScheduledAnnouncement = AnnouncementConfig.EnableScheduledAnnouncement; if (enableScheduledAnnouncement == null || !enableScheduledAnnouncement.Value || !AnnouncementManager.IsHost() || !NetworkServer.active) { break; } <interval>5__1 = AnnouncementConfig.ScheduledAnnouncementInterval?.Value ?? 300f; if (Time.time - <>4__this._lastScheduledTime < <interval>5__1) { break; } <>4__this._lastScheduledTime = Time.time; <sendToast>5__2 = AnnouncementConfig.ScheduledAnnouncementToast?.Value ?? true; <sendBanner>5__3 = AnnouncementConfig.ScheduledAnnouncementBanner?.Value ?? false; <includeHost>5__4 = AnnouncementConfig.ScheduledAnnouncementIncludeHost?.Value ?? false; <playSound>5__5 = AnnouncementConfig.ScheduledAnnouncementPlaySound?.Value ?? false; <schedSoundType>5__6 = AnnouncementConfig.ScheduledSoundIndex?.Value ?? 0; <schedSoundVolume>5__7 = AnnouncementConfig.ScheduledSoundVolume?.Value ?? 0.5f; if (<sendToast>5__2) { <message>5__8 = AnnouncementConfig.JoinToastMessage?.Value ?? "Server Announcement"; <duration>5__9 = AnnouncementConfig.ToastDuration?.Value ?? 5f; <fontSize>5__10 = AnnouncementConfig.ToastFontSize?.Value ?? 20; <>4__this.BroadcastScheduledToast(<message>5__8, <duration>5__9, <fontSize>5__10, <schedSoundType>5__6, <schedSoundVolume>5__7); if (<includeHost>5__4) { <>4__this._hostChannel.SendToastWithSoundOption(<message>5__8, <duration>5__9, <fontSize>5__10, force: true, <playSound>5__5); } <message>5__8 = null; } if (<sendBanner>5__3) { <data>5__11 = <>4__this.CreateBannerData(); <>4__this.BroadcastScheduledBanner(<data>5__11, <schedSoundType>5__6, <schedSoundVolume>5__7); if (<includeHost>5__4) { <>4__this._hostChannel.SendBannerWithSoundOption(<data>5__11, force: true, <playSound>5__5); } <data>5__11 = default(AnnouncementData); } ServerAnnouncementsPlugin.Log.LogDebug((object)$"[Router] Scheduled announcement sent. Toast: {<sendToast>5__2}, Banner: {<sendBanner>5__3}, IncludeHost: {<includeHost>5__4}, PlaySound: {<playSound>5__5}"); break; } } <>2__current = (object)new WaitForSeconds(1f); <>1__state = 1; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <SharedSchedulerCoroutine>d__30 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public AnnouncementRouter <>4__this; private ModeratorSharedContent <content>5__1; private float <interval>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SharedSchedulerCoroutine>d__30(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <content>5__1 = null; <>1__state = -2; } private bool MoveNext() { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>4__this._lastSharedScheduledTime = Time.time; break; case 1: <>1__state = -1; <content>5__1 = ModeratorContentManager.SharedContent; if (!<content>5__1.ScheduleEnabled || !AnnouncementManager.IsHost() || !NetworkServer.active) { break; } <interval>5__2 = <content>5__1.ScheduleInterval; if (<interval>5__2 < 5f) { <interval>5__2 = 5f; } if (!(Time.time - <>4__this._lastSharedScheduledTime < <interval>5__2)) { <>4__this._lastSharedScheduledTime = Time.time; if (<content>5__1.ScheduleToast) { <>4__this.BroadcastScheduledToast(<content>5__1.ToastMessage, <content>5__1.ToastDuration, <content>5__1.ToastFontSize, <content>5__1.SoundType, <content>5__1.SoundVolume); } if (<content>5__1.ScheduleBanner) { <>4__this.BroadcastSharedBanner(force: true, bypassRateLimit: true); } ServerAnnouncementsPlugin.Log.LogDebug((object)$"[Router] Shared scheduled announcement sent. Toast: {<content>5__1.ScheduleToast}, Banner: {<content>5__1.ScheduleBanner}"); <content>5__1 = null; } break; } <>2__current = (object)new WaitForSeconds(1f); <>1__state = 1; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static AnnouncementRouter _instance; private readonly HostChannel _hostChannel; private readonly ModClientChannel _modClientChannel; private readonly VanillaClientChannel _vanillaClientChannel; private float _lastBroadcastTime = 0f; private float _lastScheduledTime = 0f; private bool _schedulerRunning = false; private Coroutine _schedulerCoroutine = null; private float _lastSharedScheduledTime = 0f; private bool _sharedSchedulerRunning = false; private Coroutine _sharedSchedulerCoroutine = null; private bool _hostReceivedThisSession = false; public static AnnouncementRouter Instance => _instance ?? (_instance = new AnnouncementRouter()); public bool IsSchedulerRunning => _schedulerRunning; public bool IsSharedSchedulerRunning => _sharedSchedulerRunning; private AnnouncementRouter() { _hostChannel = new HostChannel(); _modClientChannel = new ModClientChannel(); _vanillaClientChannel = new VanillaClientChannel(); } public void ResetSession() { _hostReceivedThisSession = false; PlayerTypeManager.Reset(); StopScheduler(); StopSharedScheduler(); } public void StartScheduler() { if (!_schedulerRunning && !((Object)(object)ServerAnnouncementsPlugin.Instance == (Object)null)) { _schedulerCoroutine = ((MonoBehaviour)ServerAnnouncementsPlugin.Instance).StartCoroutine(SchedulerCoroutine()); _schedulerRunning = true; ServerAnnouncementsPlugin.Log.LogInfo((object)"[Router] Scheduled announcements started."); ModeratorContentManager.OnServerStarted(); } } public void StopScheduler() { if (_schedulerRunning) { if (_schedulerCoroutine != null && (Object)(object)ServerAnnouncementsPlugin.Instance != (Object)null) { ((MonoBehaviour)ServerAnnouncementsPlugin.Instance).StopCoroutine(_schedulerCoroutine); _schedulerCoroutine = null; } _schedulerRunning = false; ServerAnnouncementsPlugin.Log.LogInfo((object)"[Router] Scheduled announcements stopped."); } } public float GetTimeUntilNextAnnouncement() { if (!_schedulerRunning) { return -1f; } float num = AnnouncementConfig.ScheduledAnnouncementInterval?.Value ?? 300f; float num2 = Time.time - _lastScheduledTime; return Mathf.Max(0f, num - num2); } public void ResetSchedulerTimer() { _lastScheduledTime = Time.time; } [IteratorStateMachine(typeof(<SchedulerCoroutine>d__22))] private IEnumerator SchedulerCoroutine() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SchedulerCoroutine>d__22(0) { <>4__this = this }; } public void StartSharedScheduler() { if (!_sharedSchedulerRunning && !((Object)(object)ServerAnnouncementsPlugin.Instance == (Object)null)) { _sharedSchedulerCoroutine = ((MonoBehaviour)ServerAnnouncementsPlugin.Instance).StartCoroutine(SharedSchedulerCoroutine()); _sharedSchedulerRunning = true; ServerAnnouncementsPlugin.Log.LogInfo((object)"[Router] Shared content scheduler started."); } } public void StopSharedScheduler() { if (_sharedSchedulerRunning) { if (_sharedSchedulerCoroutine != null && (Object)(object)ServerAnnouncementsPlugin.Instance != (Object)null) { ((MonoBehaviour)ServerAnnouncementsPlugin.Instance).StopCoroutine(_sharedSchedulerCoroutine); _sharedSchedulerCoroutine = null; } _sharedSchedulerRunning = false; ServerAnnouncementsPlugin.Log.LogInfo((object)"[Router] Shared content scheduler stopped."); } } public float GetTimeUntilNextSharedAnnouncement() { if (!_sharedSchedulerRunning) { return -1f; } ModeratorSharedContent sharedContent = ModeratorContentManager.SharedContent; float scheduleInterval = sharedContent.ScheduleInterval; float num = Time.time - _lastSharedScheduledTime; return Mathf.Max(0f, scheduleInterval - num); } public void ResetSharedSchedulerTimer() { _lastSharedScheduledTime = Time.time; } public void UpdateSharedSchedulerState() { ModeratorSharedContent sharedContent = ModeratorContentManager.SharedContent; if (sharedContent.ScheduleEnabled && !_sharedSchedulerRunning) { StartSharedScheduler(); } else if (!sharedContent.ScheduleEnabled && _sharedSchedulerRunning) { StopSharedScheduler(); } } [IteratorStateMachine(typeof(<SharedSchedulerCoroutine>d__30))] private IEnumerator SharedSchedulerCoroutine() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SharedSchedulerCoroutine>d__30(0) { <>4__this = this }; } public void HandlePlayerConnected(int connectionId, ulong steamId = 0uL) { if (AnnouncementManager.IsHost()) { PlayerInfo playerInfo = PlayerTypeManager.RegisterPlayer(connectionId, steamId); if (playerInfo.Type != 0) { ((MonoBehaviour)ServerAnnouncementsPlugin.Instance).StartCoroutine(HandshakeAndNotifyCoroutine(connectionId)); } } } public void HandlePlayerDisconnected(int connectionId) { PlayerTypeManager.UnregisterPlayer(connectionId); } [IteratorStateMachine(typeof(<HandshakeAndNotifyCoroutine>d__33))] private IEnumerator HandshakeAndNotifyCoroutine(int connectionId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <HandshakeAndNotifyCoroutine>d__33(0) { <>4__this = this, connectionId = connectionId }; } private void SendAnnouncementsToPlayer(int connectionId) { PlayerInfo player = PlayerTypeManager.GetPlayer(connectionId); if (player == null || player.NotificationSent) { return; } float duration = AnnouncementConfig.ToastDuration?.Value ?? 10f; int fontSize = AnnouncementConfig.ToastFontSize?.Value ?? 20; if (AnnouncementConfig.ShowToastOnJoin.Value) { string value = AnnouncementConfig.JoinToastMessage.Value; if (!string.IsNullOrEmpty(value)) { _modClientChannel.SendToast(connectionId, value, duration, fontSize, force: false); if (AnnouncementConfig.VanillaCompatMode.Value) { _vanillaClientChannel.SendToast(connectionId, value, duration, fontSize, force: false); } } } if (AnnouncementConfig.ShowBannerOnJoin.Value) { AnnouncementData data = CreateBannerData(); if (!string.IsNullOrEmpty(data.Title) || !string.IsNullOrEmpty(data.Content)) { _modClientChannel.SendBanner(connectionId, data, force: false); if (AnnouncementConfig.VanillaCompatMode.Value) { _vanillaClientChannel.SendBanner(connectionId, data, force: false); } } } PlayerTypeManager.MarkNotified(connectionId); } public void BroadcastToast(string message, float duration, int fontSize, bool force = false, bool bypassRateLimit = false, bool skipLocalDisplay = false) { if (!NetworkServer.active) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] BroadcastToast called but server not active."); return; } if (!bypassRateLimit && !CheckRateLimit()) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] Broadcast rate limited."); return; } message = TruncateMessage(message); if (!skipLocalDisplay && (!_hostReceivedThisSession || force)) { _hostChannel.SendToast(0, message, duration, fontSize, force); _hostReceivedThisSession = true; } foreach (PlayerInfo client in PlayerTypeManager.GetClients()) { _modClientChannel.SendToast(client.ConnectionId, message, duration, fontSize, force); if (AnnouncementConfig.VanillaCompatMode.Value) { _vanillaClientChannel.SendToast(client.ConnectionId, message, duration, fontSize, force); } } _lastBroadcastTime = Time.time; } public void BroadcastBanner(bool force = false, bool bypassRateLimit = false, bool skipLocalDisplay = false) { if (!NetworkServer.active) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] BroadcastBanner called but server not active."); return; } if (!bypassRateLimit && !CheckRateLimit()) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] Broadcast rate limited."); return; } AnnouncementData data = CreateBannerData(); if (string.IsNullOrEmpty(data.Title) && string.IsNullOrEmpty(data.Content)) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] Banner has no content, skipping."); return; } if (!skipLocalDisplay && (!_hostReceivedThisSession || force)) { _hostChannel.SendBanner(0, data, force); _hostReceivedThisSession = true; } foreach (PlayerInfo client in PlayerTypeManager.GetClients()) { _modClientChannel.SendBanner(client.ConnectionId, data, force); if (AnnouncementConfig.VanillaCompatMode.Value) { _vanillaClientChannel.SendBanner(client.ConnectionId, data, force); } } _lastBroadcastTime = Time.time; } public void BroadcastSharedBanner(bool force = true, bool bypassRateLimit = false) { if (!NetworkServer.active) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] BroadcastSharedBanner called but server not active."); return; } if (!bypassRateLimit && !CheckRateLimit()) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] Broadcast rate limited."); return; } ModeratorSharedContent sharedContent = ModeratorContentManager.SharedContent; AnnouncementData data = new AnnouncementData(sharedContent.BannerTitle, sharedContent.BannerContent, 1, "", sharedContent.TitleFontSize, sharedContent.ContentFontSize, sharedContent.BannerExtraContent, sharedContent.ExtraFontSize, sharedContent.BannerHighlight, sharedContent.HighlightFontSize); if (string.IsNullOrEmpty(data.Title) && string.IsNullOrEmpty(data.Content)) { ServerAnnouncementsPlugin.Log.LogWarning((object)"[Router] Shared banner has no content, skipping."); return; } _hostChannel.SendBanner(0, data, force); foreach (PlayerInfo client in PlayerTypeManager.GetClients()) { _modClientChannel.SendBanner(client.ConnectionId, data, force); if (AnnouncementConfig.VanillaCompatMode.Value) { _vanillaClientChannel.SendBanner(client.ConnectionId, data, force); } } _lastBroadcastTime = Time.time; ServerAnnouncementsPlugin.Log.LogDebug((object)"[Router] Shared banner broadcasted."); } private void BroadcastScheduledToast(string message, float duration, int fontSize, int soundType, float soundVolume) { if (!NetworkServer.active) { return; } message = TruncateMessage(message); foreach (PlayerInfo client in PlayerTypeManager.GetClients()) { _modClientChannel.SendScheduledToast(client.ConnectionId, message, duration, fontSize, soundType, soundVolume); if (AnnouncementConfig.VanillaCompatMode.Value) { _vanillaClientChannel.SendToast(client.ConnectionId, message, duration, fontSize, force: true); } } } private void BroadcastScheduledBanner(AnnouncementData data, int soundType, float soundVolume) { if (!NetworkServer.active) { return; } foreach (PlayerInfo client in PlayerTypeManager.GetClients()) { _modClientChannel.SendScheduledBanner(client.ConnectionId, data, soundType, soundVolume); if (AnnouncementConfig.VanillaCompatMode.Value) { _vanillaClientChannel.SendBanner(client.ConnectionId, data, force: true); } } } public void HandleClientHandshakeResponse(int connectionId, ulong steamId) { if (connectionId > 0) { PlayerTypeManager.HandleHandshakeResponse(connectionId, steamId); } else if (steamId != 0) { PlayerTypeManager.HandleHandshakeResponseBySteamId(steamId); } } private IAnnouncementChannel GetChannelForType(PlayerType type) { return type switch { PlayerType.Host => _hostChannel, PlayerType.ModClient => _modClientChannel, _ => _vanillaClientChannel, }; } private bool CheckRateLimit() { float num = AnnouncementConfig.BroadcastCooldown?.Value ?? 3f; return Time.time - _lastBroadcastTime >= num; } private string TruncateMessage(string message) { if (!string.IsNullOrEmpty(message) && message.Length > 1000) { return message.Substring(0, 1000); } return message ?? ""; } private AnnouncementData CreateBannerData() { return new AnnouncementData(AnnouncementConfig.BannerTitle.Value ?? "", AnnouncementConfig.BannerContent.Value ?? "", AnnouncementConfig.NoticeVersion.Value, "", AnnouncementConfig.BannerTitleFontSize?.Value ?? 24, AnnouncementConfig.BannerContentFontSize?.Value ?? 16, AnnouncementConfig.BannerExtraContent?.Value ?? "", AnnouncementConfig.BannerExtraContentFontSize?.Value ?? 14, AnnouncementConfig.BannerHighlight?.Value ?? "", AnnouncementConfig.BannerHighlightFontSize?.Value ?? 18); } } public static class CodeTalkerIntegration { private static bool _initialized; private static bool _available; private static bool _uiInitialized; public static bool IsAvailable => _available; public static event Action<string, bool, string> OnModResponseReceived; public static event Action OnModeratorContentSynced; public static void Initialize() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown if (_initialized) { return; } _initialized = true; try { if (CodeTalkerNetwork.RegisterBinaryListener<AnnouncementBinaryPacket>(new BinaryPacketListener(OnPacketReceived))) { _available = true; ServerAnnouncementsPlugin.Log.LogInfo((object)"CodeTalker integration initialized successfully!"); } else { ServerAnnouncementsPlugin.Log.LogWarning((object)"CodeTalker listener registration failed."); _available = false; } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("CodeTalker integration failed: " + ex.Message)); _available = false; } } private static void EnsureUIInitialized() { if (_uiInitialized) { return; } try { AnnouncementToastUI.Initialize(); AnnouncementBannerUI.Initialize(); _uiInitialized = true; ServerAnnouncementsPlugin.Log.LogDebug((object)"UI components initialized via CodeTalker packet handler."); } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("EnsureUIInitialized error: " + ex.Message)); } } private static void OnPacketReceived(PacketHeader header, BinaryPacketBase packet) { try { if (!(packet is AnnouncementBinaryPacket announcementBinaryPacket)) { return; } if (announcementBinaryPacket.Type == "ClientResponse") { if (AnnouncementManager.IsHost()) { ulong senderID = header.SenderID; if (senderID != 0) { PlayerTypeManager.HandleHandshakeResponseBySteamId(senderID); PermissionHelper.BroadcastModeratorSync(); ModeratorContentManager.BroadcastContentSync(); } } } else if (announcementBinaryPacket.Type == "ModRequest") { if (AnnouncementManager.IsHost()) { ulong senderID2 = header.SenderID; string senderSteamId = senderID2.ToString(); ProcessModeratorRequest(announcementBinaryPacket, senderSteamId); } } else if (announcementBinaryPacket.Type == "ModContentUpdate") { if (AnnouncementManager.IsHost()) { ulong senderID2 = header.SenderID; string text = senderID2.ToString(); if (PermissionHelper.IsModerator(text)) { ProcessModeratorContentUpdate(announcementBinaryPacket, text); return; } ServerAnnouncementsPlugin.Log.LogWarning((object)("[ModContentUpdate] Permission denied for " + text)); SendModResponse(announcementBinaryPacket.RequestId ?? "content-update", success: false, "Permission denied"); } } else if (announcementBinaryPacket.Type == "ModResponse") { if (header.SenderIsLobbyOwner) { ProcessReceivedPacket(announcementBinaryPacket); } } else if (!header.SenderIsLobbyOwner) { ServerAnnouncementsPlugin.Log.LogWarning((object)("Received CodeTalker " + announcementBinaryPacket.Type + " from non-host, ignoring.")); } else { ProcessReceivedPacket(announcementBinaryPacket); } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogError((object)("Error processing CodeTalker packet: " + ex.Message)); } } private static void ProcessModeratorRequest(AnnouncementBinaryPacket packet, string senderSteamId) { if (!PermissionHelper.IsModerator(senderSteamId)) { ServerAnnouncementsPlugin.Log.LogWarning((object)("[ModRequest] Permission denied for " + senderSteamId)); SendModResponse(packet.RequestId, success: false, "Permission denied"); return; } try { string title = packet.Title; if (title == "Toast") { AnnouncementRouter.Instance.BroadcastToast(packet.Content, packet.Duration, packet.ToastFontSize, packet.ForceShow); SendModResponse(packet.RequestId, success: true, ""); } else if (title == "Banner") { AnnouncementRouter.Instance.BroadcastSharedBanner(); SendModResponse(packet.RequestId, success: true, ""); } else { ServerAnnouncementsPlugin.Log.LogWarning((object)("[ModRequest] Unknown request type: " + title)); SendModResponse(packet.RequestId, success: false, "Unknown request type: " + title); } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogError((object)("[ModRequest] Error: " + ex.Message)); SendModResponse(packet.RequestId, success: false, ex.Message); } } private static void SendModResponse(string requestId, bool success, string errorMessage) { if (_available) { AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "ModResponse", RequestId = requestId, Success = success, ErrorMessage = (errorMessage ?? "") }; SendPacket(packet); } } private static void ProcessReceivedPacket(AnnouncementBinaryPacket packet) { EnsureUIInitialized(); switch (packet.Type) { case "Toast": AnnouncementManager.Instance?.HandleToastReceived(packet.Content, packet.Duration, packet.ToastFontSize, packet.ForceShow, null, packet.SoundType, packet.SoundVolume); break; case "Banner": { AnnouncementData data = new AnnouncementData(packet.Title, packet.Content, packet.Version, "", packet.TitleFontSize, packet.ContentFontSize, packet.ExtraContent, packet.ExtraFontSize, packet.Highlight, packet.HighlightFontSize); AnnouncementManager.Instance?.HandleBannerReceived(data, packet.ForceShow, null, packet.SoundType, packet.SoundVolume); break; } case "Handshake": AnnouncementNetworkHandler.HandleHandshake("ServerAnnouncements_v2_CT"); SendClientResponse(); break; case "ModResponse": HandleModResponse(packet); break; case "PermSync": HandlePermissionSync(packet); break; case "ModContentSync": HandleModeratorContentSync(packet); break; default: ServerAnnouncementsPlugin.Log.LogWarning((object)("Unknown announcement type: " + packet.Type)); break; } } private static void HandlePermissionSync(AnnouncementBinaryPacket packet) { string text = packet.Content ?? ""; string[] moderatorIds = text.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); PermissionHelper.UpdateSyncedModeratorList(moderatorIds); } private static void HandleModeratorContentSync(AnnouncementBinaryPacket packet) { string serializedData = packet.ExtraContent ?? ""; ModeratorContentManager.UpdateFromSync(serializedData); CodeTalkerIntegration.OnModeratorContentSynced?.Invoke(); } private static void ProcessModeratorContentUpdate(AnnouncementBinaryPacket packet, string senderSteamId) { string data = packet.ExtraContent ?? ""; ModeratorSharedContent content = ModeratorSharedContent.Deserialize(data); ModeratorContentManager.UpdateContent(content, senderSteamId); } private static void HandleModResponse(AnnouncementBinaryPacket packet) { if (!packet.Success && packet.ErrorMessage != null && packet.ErrorMessage.Contains("Permission denied")) { PermissionHelper.ClearSyncedModeratorList(); } CodeTalkerIntegration.OnModResponseReceived?.Invoke(packet.RequestId, packet.Success, packet.ErrorMessage); } private static bool SendPacket(AnnouncementBinaryPacket packet) { if (!_available) { return false; } try { CodeTalkerNetwork.SendNetworkPacket((BinaryPacketBase)(object)packet); return true; } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogError((object)("Failed to send CodeTalker packet: " + ex.Message)); return false; } } public static bool BroadcastToast(string message, float duration, int fontSize, bool forceShow) { if (!_available) { return false; } AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "Toast", Content = (message ?? ""), Duration = duration, ToastFontSize = fontSize, ForceShow = forceShow }; return SendPacket(packet); } public static bool BroadcastBanner(string title, string content, string extraContent, int version, int titleFontSize, int contentFontSize, int extraFontSize, bool forceShow) { if (!_available) { return false; } AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "Banner", Title = (title ?? ""), Content = (content ?? ""), ExtraContent = (extraContent ?? ""), Version = version, TitleFontSize = titleFontSize, ContentFontSize = contentFontSize, ExtraFontSize = extraFontSize, ForceShow = forceShow }; return SendPacket(packet); } public static bool SendHandshake() { if (!_available) { return false; } AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "Handshake", Content = "ServerAnnouncements_v1" }; return SendPacket(packet); } public static bool SendClientResponse() { if (!_available) { return false; } int num = -1; try { if (NetworkClient.connection != null) { num = NetworkClient.connection.connectionId; } } catch { } AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "ClientResponse", Content = num.ToString() }; return SendPacket(packet); } public static string SendModRequest(string requestType, string message, float duration, int fontSize, bool forceShow) { if (!_available) { return null; } if (!PermissionHelper.CanSendAnnouncements()) { return null; } string text = Guid.NewGuid().ToString("N").Substring(0, 8); AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "ModRequest", Title = requestType, Content = (message ?? ""), Duration = duration, ToastFontSize = fontSize, ForceShow = forceShow, RequestId = text }; if (SendPacket(packet)) { return text; } return null; } public static bool BroadcastPermissionSync(string moderatorList) { if (!_available) { return false; } if (!AnnouncementManager.IsHost()) { return false; } AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "PermSync", Content = (moderatorList ?? "") }; return SendPacket(packet); } public static bool BroadcastModeratorContentSync(string serializedContent) { if (!_available) { return false; } if (!AnnouncementManager.IsHost()) { return false; } AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "ModContentSync", ExtraContent = (serializedContent ?? "") }; return SendPacket(packet); } public static bool SendModeratorContentUpdate(string serializedContent) { if (!_available) { return false; } if (!PermissionHelper.CanSendAnnouncements()) { return false; } AnnouncementBinaryPacket packet = new AnnouncementBinaryPacket { Type = "ModContentUpdate", ExtraContent = (serializedContent ?? "") }; return SendPacket(packet); } } public class AnnouncementBinaryPacket : BinaryPacketBase { public override string PacketSignature => "SA_ANN_v2"; public string Type { get; set; } = ""; public string Title { get; set; } = ""; public string Content { get; set; } = ""; public string ExtraContent { get; set; } = ""; public int Version { get; set; } public float Duration { get; set; } public int TitleFontSize { get; set; } public int ContentFontSize { get; set; } public int ExtraFontSize { get; set; } public int ToastFontSize { get; set; } public bool ForceShow { get; set; } public string RequestId { get; set; } = ""; public bool Success { get; set; } public string ErrorMessage { get; set; } = ""; public int SoundType { get; set; } public float SoundVolume { get; set; } = 0.5f; public string Highlight { get; set; } = ""; public int HighlightFontSize { get; set; } public override byte[] Serialize() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(EscapePipe(Type)); stringBuilder.Append("|"); stringBuilder.Append(EscapePipe(Title)); stringBuilder.Append("|"); stringBuilder.Append(EscapePipe(Content)); stringBuilder.Append("|"); stringBuilder.Append(EscapePipe(ExtraContent)); stringBuilder.Append("|"); stringBuilder.Append(Version); stringBuilder.Append("|"); stringBuilder.Append(Duration.ToString("F2")); stringBuilder.Append("|"); stringBuilder.Append(TitleFontSize); stringBuilder.Append("|"); stringBuilder.Append(ContentFontSize); stringBuilder.Append("|"); stringBuilder.Append(ExtraFontSize); stringBuilder.Append("|"); stringBuilder.Append(ToastFontSize); stringBuilder.Append("|"); stringBuilder.Append(ForceShow ? "1" : "0"); stringBuilder.Append("|"); stringBuilder.Append(EscapePipe(RequestId)); stringBuilder.Append("|"); stringBuilder.Append(Success ? "1" : "0"); stringBuilder.Append("|"); stringBuilder.Append(EscapePipe(ErrorMessage)); stringBuilder.Append("|"); stringBuilder.Append(SoundType); stringBuilder.Append("|"); stringBuilder.Append(SoundVolume.ToString("F2")); stringBuilder.Append("|"); stringBuilder.Append(EscapePipe(Highlight)); stringBuilder.Append("|"); stringBuilder.Append(HighlightFontSize); return Encoding.UTF8.GetBytes(stringBuilder.ToString()); static string EscapePipe(string s) { return (s ?? "").Replace("|", "\\|").Replace("\n", "\\n"); } } public override void Deserialize(byte[] data) { try { string @string = Encoding.UTF8.GetString(data); string[] array = SplitByUnescapedPipe(@string); if (array.Length >= 11) { Type = UnescapePipe(array[0]); Title = UnescapePipe(array[1]); Content = UnescapePipe(array[2]); ExtraContent = UnescapePipe(array[3]); int.TryParse(array[4], out var result); Version = result; float.TryParse(array[5], out var result2); Duration = result2; int.TryParse(array[6], out var result3); TitleFontSize = result3; int.TryParse(array[7], out var result4); ContentFontSize = result4; int.TryParse(array[8], out var result5); ExtraFontSize = result5; int.TryParse(array[9], out var result6); ToastFontSize = result6; ForceShow = array[10] == "1"; } if (array.Length >= 18) { RequestId = UnescapePipe(array[11]); Success = array[12] == "1"; ErrorMessage = UnescapePipe(array[13]); int.TryParse(array[14], out var result7); SoundType = result7; float.TryParse(array[15], out var result8); SoundVolume = result8; Highlight = UnescapePipe(array[16]); int.TryParse(array[17], out var result9); HighlightFontSize = result9; } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogError((object)("AnnouncementBinaryPacket.Deserialize error: " + ex.Message)); } static string UnescapePipe(string s) { return (s ?? "").Replace("\\|", "|").Replace("\\n", "\n"); } } private static string[] SplitByUnescapedPipe(string input) { List<string> list = new List<string>(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < input.Length; i++) { if (input[i] == '|' && (i == 0 || input[i - 1] != '\\')) { list.Add(stringBuilder.ToString()); stringBuilder.Clear(); } else { stringBuilder.Append(input[i]); } } list.Add(stringBuilder.ToString()); return list.ToArray(); } } public static class AnnouncementNetworkHandler { private static bool _initialized; private static bool _serverHasNoticeMod; private static string _serverModSignature; private const string HANDSHAKE_PREFIX = "[NTC:HS]"; private const string TOAST_PREFIX = "[NTC:T]"; private const string BANNER_PREFIX = "[NTC:B]"; private const string CLIENT_RESPONSE_PREFIX = "[NTC:CR]"; public static bool ServerHasNoticeMod => _serverHasNoticeMod; public static void Initialize() { if (!_initialized) { _initialized = true; } } public static void ResetHandshake() { _serverHasNoticeMod = false; _serverModSignature = null; AnnouncementRouter.Instance.ResetSession(); } public static void HandleHandshake(string signature) { _serverHasNoticeMod = true; _serverModSignature = signature; } public static bool IsSoloMode() { try { Type type = Type.GetType("AtlyssNetworkManager, Assembly-CSharp"); if (type != null) { FieldInfo field = type.GetField("_current", BindingFlags.Static | BindingFlags.Public); if (field != null) { object value = field.GetValue(null); if (value != null) { FieldInfo field2 = type.GetField("_soloMode", BindingFlags.Instance | BindingFlags.Public); if (field2 != null) { return (bool)field2.GetValue(value); } } } } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("IsSoloMode check failed: " + ex.Message)); } return false; } public static void BroadcastToast(string message, float duration, bool skipLocalDisplay = false, bool forceShow = false, bool bypassRateLimit = false, bool skipVanillaCompat = false) { int fontSize = AnnouncementConfig.ToastFontSize?.Value ?? 20; AnnouncementRouter.Instance.BroadcastToast(message, duration, fontSize, forceShow, bypassRateLimit, skipLocalDisplay); } public static void BroadcastBanner(bool forceShow = false, bool skipLocalDisplay = false, bool bypassRateLimit = false, bool skipVanillaCompat = false) { AnnouncementRouter.Instance.BroadcastBanner(forceShow, bypassRateLimit, skipLocalDisplay); } public static void SendHandshake() { } public static bool TryParseHandshake(string message, out string signature) { signature = null; if (string.IsNullOrEmpty(message) || !message.StartsWith("[NTC:HS]")) { return false; } signature = message.Substring("[NTC:HS]".Length); if (signature.EndsWith("<>")) { signature = signature.Substring(0, signature.Length - 2); } return !string.IsNullOrEmpty(signature); } public static bool TryParseToast(string message, out string text, out float duration, out int fontSize, out bool forceShow, out int soundType, out float soundVolume, out bool isScheduled) { text = null; duration = 0f; fontSize = 20; forceShow = false; soundType = 0; soundVolume = 0.5f; isScheduled = false; if (string.IsNullOrEmpty(message) || !message.StartsWith("[NTC:T]")) { return false; } try { string text2 = message.Substring("[NTC:T]".Length); if (text2.EndsWith("<>")) { text2 = text2.Substring(0, text2.Length - 2); } if (text2.EndsWith("|SCHED")) { isScheduled = true; forceShow = true; text2 = text2.Substring(0, text2.Length - 6); } else if (text2.EndsWith("|FORCE")) { forceShow = true; text2 = text2.Substring(0, text2.Length - 6); } string[] array = text2.Split(new char[1] { '|' }, 5); if (array.Length >= 5) { if (!float.TryParse(array[0], out duration)) { return false; } if (!int.TryParse(array[1], out fontSize)) { fontSize = 20; } if (!int.TryParse(array[2], out soundType)) { soundType = 0; } if (!float.TryParse(array[3], out soundVolume)) { soundVolume = 0.5f; } text = array[4]; return true; } if (array.Length >= 3) { if (!float.TryParse(array[0], out duration)) { return false; } if (!int.TryParse(array[1], out fontSize)) { fontSize = 20; } text = array[2]; return true; } if (array.Length >= 2) { if (!float.TryParse(array[0], out duration)) { return false; } text = array[1]; return true; } return false; } catch { return false; } } public static bool TryParseBanner(string message, out AnnouncementData data, out bool forceShow, out int soundType, out float soundVolume, out bool isScheduled) { data = default(AnnouncementData); soundType = 0; soundVolume = 0.5f; forceShow = false; isScheduled = false; if (string.IsNullOrEmpty(message) || !message.StartsWith("[NTC:B]")) { return false; } try { string text = message.Substring("[NTC:B]".Length); if (text.EndsWith("<>")) { text = text.Substring(0, text.Length - 2); } if (text.EndsWith("|SCHED")) { isScheduled = true; forceShow = true; text = text.Substring(0, text.Length - 6); } else if (text.EndsWith("|FORCE")) { forceShow = true; text = text.Substring(0, text.Length - 6); } string[] array = text.Split(new char[1] { '|' }, 11); if (array.Length >= 11) { if (!int.TryParse(array[0], out var result)) { return false; } if (!int.TryParse(array[1], out var result2)) { result2 = 24; } if (!int.TryParse(array[2], out var result3)) { result3 = 16; } if (!int.TryParse(array[3], out var result4)) { result4 = 14; } if (!int.TryParse(array[4], out var result5)) { result5 = 18; } if (!int.TryParse(array[5], out soundType)) { soundType = 0; } if (!float.TryParse(array[6], out soundVolume)) { soundVolume = 0.5f; } data = new AnnouncementData(array[7], array[9].Replace("\\|", "|"), result, "", result2, result3, array[10].Replace("\\|", "|"), result4, array[8].Replace("\\|", "|"), result5); return true; } if (array.Length >= 9) { if (!int.TryParse(array[0], out var result6)) { return false; } if (!int.TryParse(array[1], out var result7)) { result7 = 24; } if (!int.TryParse(array[2], out var result8)) { result8 = 16; } if (!int.TryParse(array[3], out var result9)) { result9 = 14; } if (!int.TryParse(array[4], out var result10)) { result10 = 18; } data = new AnnouncementData(array[5], array[7].Replace("\\|", "|"), result6, "", result7, result8, array[8].Replace("\\|", "|"), result9, array[6].Replace("\\|", "|"), result10); return true; } if (array.Length >= 7) { if (!int.TryParse(array[0], out var result11)) { return false; } if (!int.TryParse(array[1], out var result12)) { result12 = 24; } if (!int.TryParse(array[2], out var result13)) { result13 = 16; } if (!int.TryParse(array[3], out var result14)) { result14 = 14; } data = new AnnouncementData(array[4], array[5].Replace("\\|", "|"), result11, "", result12, result13, array[6].Replace("\\|", "|"), result14); return true; } if (array.Length >= 5) { if (!int.TryParse(array[0], out var result15)) { return false; } if (!int.TryParse(array[1], out var result16)) { result16 = 24; } if (!int.TryParse(array[2], out var result17)) { result17 = 16; } data = new AnnouncementData(array[3], array[4].Replace("\\|", "|"), result15, "", result16, result17); return true; } if (array.Length >= 3) { if (!int.TryParse(array[0], out var result18)) { return false; } data = new AnnouncementData(array[1], array[2].Replace("\\|", "|"), result18); return true; } return false; } catch { return false; } } } [HarmonyPatch] public static class NoticeNetworkPatches { private static readonly string[] VANILLA_PREFIXES = new string[4] { "[Notice]", "[Banner]", " > ", " * " }; private static bool _uiInitialized = false; [HarmonyPatch(typeof(ChatBehaviour), "New_ChatMessage")] [HarmonyPrefix] private static bool OnReceiveChatMessage(string _message) { if (string.IsNullOrEmpty(_message)) { return true; } try { string[] vANILLA_PREFIXES = VANILLA_PREFIXES; foreach (string value in vANILLA_PREFIXES) { if (_message.Contains(value)) { return false; } } if (AnnouncementNetworkHandler.TryParseToast(_message, out var text, out var duration, out var fontSize, out var forceShow, out var soundType, out var soundVolume, out var isScheduled)) { AnnouncementManager.Instance?.HandleToastReceived(text, duration, fontSize, forceShow, null, soundType, soundVolume); AnnouncementManager.Instance?.SetLastIsScheduled(isScheduled); return false; } if (AnnouncementNetworkHandler.TryParseBanner(_message, out var data, out var forceShow2, out var soundType2, out var soundVolume2, out var isScheduled2)) { AnnouncementManager.Instance?.HandleBannerReceived(data, forceShow2, null, soundType2, soundVolume2); AnnouncementManager.Instance?.SetLastIsScheduled(isScheduled2); return false; } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("OnReceiveChatMessage error: " + ex.Message)); } return true; } [HarmonyPatch(typeof(ChatBehaviour), "OnServerMessage")] [HarmonyPrefix] private static bool OnServerMessageReceived(ref object __0) { try { if (__0 == null) { return true; } Type type = __0.GetType(); FieldInfo field = type.GetField("servMsg"); if (field == null) { return true; } string text = field.GetValue(__0) as string; if (string.IsNullOrEmpty(text)) { return true; } EnsureUIInitialized(); string[] vANILLA_PREFIXES = VANILLA_PREFIXES; foreach (string value in vANILLA_PREFIXES) { if (text.Contains(value)) { return false; } } if (AnnouncementNetworkHandler.TryParseHandshake(text, out var signature)) { AnnouncementNetworkHandler.HandleHandshake(signature); SendClientResponse(); return false; } if (text.StartsWith("[NTC:") && !AnnouncementNetworkHandler.ServerHasNoticeMod) { AnnouncementNetworkHandler.HandleHandshake("ServerAnnouncements_Auto"); } if (AnnouncementManager.IsHost() && (text.StartsWith("[NTC:T]") || text.StartsWith("[NTC:B]"))) { return false; } if (AnnouncementNetworkHandler.TryParseToast(text, out var text2, out var duration, out var fontSize, out var forceShow, out var soundType, out var soundVolume, out var isScheduled)) { AnnouncementManager.Instance?.HandleToastReceived(text2, duration, fontSize, forceShow, null, soundType, soundVolume); AnnouncementManager.Instance?.SetLastIsScheduled(isScheduled); return false; } if (AnnouncementNetworkHandler.TryParseBanner(text, out var data, out var forceShow2, out var soundType2, out var soundVolume2, out var isScheduled2)) { AnnouncementManager.Instance?.HandleBannerReceived(data, forceShow2, null, soundType2, soundVolume2); AnnouncementManager.Instance?.SetLastIsScheduled(isScheduled2); return false; } } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("OnServerMessageReceived error: " + ex.Message)); } return true; } private static void EnsureUIInitialized() { if (_uiInitialized) { return; } try { AnnouncementToastUI.Initialize(); AnnouncementBannerUI.Initialize(); _uiInitialized = true; } catch (Exception ex) { ServerAnnouncementsPlugin.Log.LogWarning((object)("EnsureUIInitialized error: " + ex.Message)); } } private static void SendClientResponse() { CodeTalkerIntegration.SendClientResponse(); } } public enum PlayerType { Host, ModClient, VanillaClient, Pending } public class PlayerInfo { public int ConnectionId { get; set; } public ulong SteamId { get; set; } public PlayerType Type { get; set; } public DateTime ConnectedAt { get; set; } public DateTime? HandshakeSentAt { get; set; } public bool HandshakeResponseReceived { get; set; } public bool NotificationSent { get; set; } public string CharacterName { get; set; } public string GlobalNickname { get; set; } public PlayerInfo(int connectionId, ulong steamId = 0uL) { ConnectionId = connectionId; SteamId = steamId; Type = ((connectionId != 0) ? PlayerType.Pending : PlayerType.Host); ConnectedAt = DateTime.Now; HandshakeSentAt = null; HandshakeResponseReceived = false; NotificationSent = false; CharacterName = ""; GlobalNickname = ""; } public string GetDisplayString() { if (!string.IsNullOrEmpty(CharacterName) || !string.IsNullOrEmpty(GlobalNickname)) { List<string> list = new List<string>(); if (!string.IsNullOrEmpty(CharacterName)) { list.Add(CharacterName); } if (!string.IsNullOrEmpty(GlobalNickname) && GlobalNickname != CharacterName) { list.Add("(" + GlobalNickname + ")"); } if (SteamId != 0) { list.Add($"[{SteamId}]"); } return (list.Count > 0) ? string.Join(" ", list) : $"[{SteamId}]"; } if (SteamId != 0) { return PlayerTypeManager.GetDisplayStringBySteamId(SteamId.ToString()); } return $"[{SteamId}]"; } } public static class PlayerTypeManager {