using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; [InitializeOnLoad] public static class SceneNotifier { private static readonly string WebhookUrl = "https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN"; private static readonly string UserName = Environment.UserName; private static readonly string MachineName = Environment.MachineName; private static readonly HttpClient Http = new HttpClient(); private static readonly Dictionary OpenScenes = new Dictionary(); // Suppresses fake open/close events caused by Play Mode transitions private static bool SuppressSceneEvents = false; static SceneNotifier() { EditorSceneManager.sceneOpened += OnSceneOpened; EditorSceneManager.sceneClosed += OnSceneClosed; EditorApplication.quitting += OnEditorQuitting; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } private static void OnPlayModeStateChanged(PlayModeStateChange state) { switch (state) { case PlayModeStateChange.ExitingEditMode: case PlayModeStateChange.EnteredPlayMode: case PlayModeStateChange.ExitingPlayMode: case PlayModeStateChange.EnteredEditMode: SuppressSceneEvents = true; break; } // Re-enable notifications shortly after returning to edit mode if (state == PlayModeStateChange.EnteredEditMode) { EditorApplication.delayCall += () => { SuppressSceneEvents = false; }; } } private static void OnSceneOpened(UnityEngine.SceneManagement.Scene scene, OpenSceneMode mode) { if (SuppressSceneEvents || EditorApplication.isPlayingOrWillChangePlaymode) return; if (!IsTrackableScene(scene)) return; string scenePath = scene.path; DateTime openedAt = DateTime.Now; OpenScenes[scenePath] = openedAt; string message = $"🚧 **Scene Locked**\n" + $"👤 **{UserName}**\n" + $"💻 **{MachineName}**\n" + $"📁 `{FormatPath(scenePath)}`\n" + $"🕒 Opened: **{FormatDateTime(openedAt)}**"; _ = SendDiscordMessage(message); } private static void OnSceneClosed(UnityEngine.SceneManagement.Scene scene) { if (SuppressSceneEvents || EditorApplication.isPlayingOrWillChangePlaymode) return; if (!IsTrackableScene(scene)) return; string scenePath = scene.path; DateTime closedAt = DateTime.Now; if (OpenScenes.TryGetValue(scenePath, out DateTime openedAt)) { TimeSpan duration = closedAt - openedAt; string message = $"🔓 **Scene Released**\n" + $"👤 **{UserName}**\n" + $"💻 **{MachineName}**\n" + $"📁 `{FormatPath(scenePath)}`\n" + $"🕒 Opened: **{FormatDateTime(openedAt)}**\n" + $"🕒 Closed: **{FormatDateTime(closedAt)}**\n" + $"⏱ Stayed on scene: **{FormatDuration(duration)}**"; OpenScenes.Remove(scenePath); _ = SendDiscordMessage(message); } } private static void OnEditorQuitting() { DateTime quitAt = DateTime.Now; if (OpenScenes.Count == 0) return; var scenesToRelease = new List>(OpenScenes); foreach (var kvp in scenesToRelease) { string scenePath = kvp.Key; DateTime openedAt = kvp.Value; TimeSpan duration = quitAt - openedAt; string message = $"❌ **Unity Closed — Scene Auto-Released**\n" + $"👤 **{UserName}**\n" + $"💻 **{MachineName}**\n" + $"📁 `{FormatPath(scenePath)}`\n" + $"🕒 Opened: **{FormatDateTime(openedAt)}**\n" + $"🕒 Unity closed: **{FormatDateTime(quitAt)}**\n" + $"⏱ Stayed on scene: **{FormatDuration(duration)}**"; _ = SendDiscordMessage(message); } OpenScenes.Clear(); } private static bool IsTrackableScene(UnityEngine.SceneManagement.Scene scene) { if (string.IsNullOrWhiteSpace(scene.path)) return false; if (!scene.path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) return false; return true; } private static string FormatPath(string path) { return path.Replace("Assets/", ""); } private static string FormatDateTime(DateTime dt) { return dt.ToString("yyyy-MM-dd HH:mm:ss"); } private static string FormatDuration(TimeSpan duration) { if (duration.TotalSeconds < 60) return $"{(int)duration.TotalSeconds}s"; if (duration.TotalMinutes < 60) return $"{(int)duration.TotalMinutes}m {duration.Seconds}s"; return $"{(int)duration.TotalHours}h {duration.Minutes}m {duration.Seconds}s"; } private static async Task SendDiscordMessage(string content) { try { string escapedContent = EscapeJson(content); string json = $"{{\"content\":\"{escapedContent}\"}}"; using var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); using var response = await Http.PostAsync(WebhookUrl, httpContent); if (!response.IsSuccessStatusCode) { string responseBody = await response.Content.ReadAsStringAsync(); Debug.LogWarning( $"[SceneNotifier] Discord webhook failed. Status: {(int)response.StatusCode} {response.StatusCode}\n{responseBody}" ); } } catch (Exception e) { Debug.LogWarning($"[SceneNotifier] Failed to notify Discord: {e.Message}"); } } private static string EscapeJson(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; return value .Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\r", "\\r") .Replace("\n", "\\n") .Replace("\t", "\\t"); } }