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 { // Replace with your Discord webhook URL private static readonly string WebhookUrl = "https://discord.com/api/webhooks/1486718530765328424/42mP_zYNTEwwBbc_lcuasPbpZppXoC4U14-Pde1YatdxatT5s9vml8lgQXTwAmm0PNe5"; private static readonly string UserName = Environment.UserName; private static readonly string MachineName = Environment.MachineName; private static readonly HttpClient Http = new HttpClient(); // Key = full scene path, Value = opened timestamp private static readonly Dictionary OpenScenes = new Dictionary(); static SceneNotifier() { EditorSceneManager.sceneOpened += OnSceneOpened; EditorSceneManager.sceneClosed += OnSceneClosed; EditorApplication.quitting += OnEditorQuitting; } private static void OnSceneOpened(UnityEngine.SceneManagement.Scene scene, OpenSceneMode mode) { 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 (!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); } else { // Safety fallback in case Unity reports a close for a scene we did not track string message = $"🔓 **Scene Released**\n" + $"👤 **{UserName}**\n" + $"💻 **{MachineName}**\n" + $"📁 `{FormatPath(scenePath)}`\n" + $"🕒 Closed: **{FormatDateTime(closedAt)}**\n" + $"⏱ Stayed on scene: **unknown**"; _ = SendDiscordMessage(message); } } private static void OnEditorQuitting() { DateTime quitAt = DateTime.Now; if (OpenScenes.Count == 0) { string message = $"❌ **Unity Closed**\n" + $"👤 **{UserName}**\n" + $"💻 **{MachineName}**\n" + $"🕒 Closed Unity: **{FormatDateTime(quitAt)}**\n" + $"📂 No tracked open scenes"; _ = SendDiscordMessage(message); return; } // Copy first so we can safely clear afterward 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) { // Prevent false notifications on startup / editor internals / untitled scenes 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) { Debug.Log($"[SceneNotifier] Sent to Discord:\n{content}"); } else { 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"); } }