using System; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; [InitializeOnLoad] public static class SceneEditPresenceTool { private const string ServerUrl = "http://unityapi.coiriergraphics.fr/scene_presence/"; private const double HeartbeatInterval = 180; private static string _userId; private static string _projectName; private static string _scenePath; private static double _nextHeartbeat; private static bool _offline; private static string _currentEditor = "none"; private static bool _requestInFlight; private const float OverlayHeight = 30f; private const float OverlayMarginTop = 3f; private const float OverlayOffsetX = 48f; private const float OverlayBaseWidth = 420f; private const float OverlayWidthScale = .70f; private const float ButtonWidth = 78f; private static string ConfigPath => Path.Combine( Directory.GetParent(Application.dataPath).FullName, "UserSettings", "ScenePresenceUser.json" ); static SceneEditPresenceTool() { _projectName = "Unheimlich_POC";// Directory.GetParent(Application.dataPath).Name; _userId = LoadOrAskUserId(); EditorApplication.delayCall += OnStartup; EditorApplication.update += OnUpdate; EditorSceneManager.activeSceneChangedInEditMode += OnSceneChanged; SceneView.duringSceneGui += OnSceneGui; } private static async void OnStartup() { if (string.IsNullOrWhiteSpace(_userId)) return; var scene = SceneManager.GetActiveScene(); if (!string.IsNullOrEmpty(scene.path)) { _scenePath = scene.path; await OpenScene(); } } private static async void OnSceneChanged(Scene oldScene, Scene newScene) { if (string.IsNullOrWhiteSpace(_userId)) return; if (!string.IsNullOrEmpty(newScene.path)) { _scenePath = newScene.path; await OpenScene(); } } private static async void OnUpdate() { if (string.IsNullOrWhiteSpace(_userId)) return; if (string.IsNullOrEmpty(_scenePath)) return; if (_requestInFlight) return; if (EditorApplication.timeSinceStartup < _nextHeartbeat) return; _nextHeartbeat = EditorApplication.timeSinceStartup + HeartbeatInterval; await Heartbeat(); RepaintSceneView(); } private static void OnSceneGui(SceneView sceneView) { Handles.BeginGUI(); float width = Mathf.Min(sceneView.position.width - (OverlayOffsetX + 16f), OverlayBaseWidth) * OverlayWidthScale; Rect rect = new Rect( OverlayOffsetX, OverlayMarginTop, width, OverlayHeight ); DrawTopBar(rect); Handles.EndGUI(); } private static void DrawTopBar(Rect rect) { UiState state = GetUiState(); string buttonLabel = state == UiState.OwnedByMe ? "Release" : "Edit"; bool buttonEnabled = state != UiState.Offline && !_requestInFlight; Rect buttonRect = new Rect( rect.xMax - ButtonWidth - 6f, rect.y + 4f, ButtonWidth, rect.height - 8f ); Rect textRect = new Rect( rect.x + 10f, rect.y, rect.width - ButtonWidth - 22f, rect.height ); EditorGUI.DrawRect(rect, GetBackgroundColor(state)); GUIStyle textStyle = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft, fontStyle = FontStyle.Bold, fontSize = 11, normal = { textColor = Color.white }, clipping = TextClipping.Clip }; GUI.Label(textRect, GetMessage(state), textStyle); Color oldColor = GUI.backgroundColor; GUI.backgroundColor = GetButtonColor(state); EditorGUI.BeginDisabledGroup(!buttonEnabled); if (GUI.Button(buttonRect, buttonLabel)) { if (state == UiState.MissingUserId) { AskUserIdWindow.ShowWindow(SaveUserIdAndRefresh); } else if (state == UiState.OwnedByMe) { ReleaseEditMode(); } else { TakeEditMode(); } } EditorGUI.EndDisabledGroup(); GUI.backgroundColor = oldColor; } private static UiState GetUiState() { if (string.IsNullOrWhiteSpace(_userId)) return UiState.MissingUserId; if (_offline) return UiState.Offline; if (_currentEditor == _userId) return UiState.OwnedByMe; if (!string.IsNullOrEmpty(_currentEditor) && _currentEditor != "none") return UiState.OwnedByOther; return UiState.Free; } private static string GetMessage(UiState state) { switch (state) { case UiState.MissingUserId: return "User ID required"; case UiState.OwnedByMe: return "You are editing this scene"; case UiState.OwnedByOther: return $"{_currentEditor} is editing this scene"; case UiState.Offline: return "Offline"; default: return "Scene available"; } } private static Color GetBackgroundColor(UiState state) { const float alpha = 0.70f; switch (state) { case UiState.MissingUserId: case UiState.Offline: return new Color(1.00f, 0.10f, 0.10f, alpha); case UiState.OwnedByMe: return new Color(0.22f, 0.55f, 0.28f, alpha); case UiState.OwnedByOther: return new Color(0.82f, 0.48f, 0.10f, alpha); default: return new Color(0.18f, 0.18f, 0.18f, alpha); } } private static Color GetButtonColor(UiState state) { switch (state) { case UiState.Free: return new Color(0.38f, 0.66f, 1.00f, 1f); case UiState.MissingUserId: case UiState.OwnedByOther: case UiState.OwnedByMe: case UiState.Offline: default: return new Color(0.22f, 0.22f, 0.22f, 1f); } } private static async void TakeEditMode() { if (string.IsNullOrWhiteSpace(_userId)) { AskUserIdWindow.ShowWindow(SaveUserIdAndRefresh); return; } if (string.IsNullOrEmpty(_scenePath)) return; EditModeRequest payload = new EditModeRequest { projectName = _projectName, scenePath = _scenePath, userId = _userId, dateTime = DateTime.UtcNow.ToString("o"), force = false }; SetEditModeResponse result = await Request("set_edit_mode.php", payload); if (result == null) { SetOffline(); return; } _offline = false; if (!result.success) { Debug.LogError("[ScenePresence] set_edit_mode failed."); RepaintSceneView(); return; } if (result.granted) { _currentEditor = _userId; RepaintSceneView(); return; } string ownerName = !string.IsNullOrWhiteSpace(result.occupiedBy) ? result.occupiedBy : (!string.IsNullOrWhiteSpace(_currentEditor) && _currentEditor != "none" ? _currentEditor : "another user"); bool forceTake = EditorUtility.DisplayDialog( "Scene Already In Edit Mode", $"This scene is already locked by {ownerName}.\nDo you want to take control anyway?", "Take Control", "Cancel" ); if (!forceTake) return; payload.force = true; SetEditModeResponse forced = await Request("set_edit_mode.php", payload); if (forced == null) { SetOffline(); return; } _offline = false; if (forced.success && forced.granted) { _currentEditor = _userId; RepaintSceneView(); } } private static async void ReleaseEditMode() { if (string.IsNullOrWhiteSpace(_userId)) return; if (string.IsNullOrEmpty(_scenePath)) return; ReleaseResponse result = await Request("release_edit_mode.php", new BaseRequest { projectName = _projectName, scenePath = _scenePath, userId = _userId, dateTime = DateTime.UtcNow.ToString("o") }); if (result == null) { SetOffline(); return; } _offline = false; _currentEditor = "none"; RepaintSceneView(); await OpenScene(); } private static async Task OpenScene() { OpenSceneResponse result = await Request("open_scene.php", new BaseRequest { projectName = _projectName, scenePath = _scenePath, userId = _userId, dateTime = DateTime.UtcNow.ToString("o") }); if (result == null) { SetOffline(); return; } _offline = false; if (result.hasEditor) { _currentEditor = string.IsNullOrWhiteSpace(result.editorUserId) ? "none" : result.editorUserId; } else { _currentEditor = "none"; } RepaintSceneView(); } private static async Task Heartbeat() { HeartbeatResponse result = await Request("heartbeat.php", new BaseRequest { projectName = _projectName, scenePath = _scenePath, userId = _userId, dateTime = DateTime.UtcNow.ToString("o") }); if (result == null) { SetOffline(); return; } _offline = false; if (result.hasEditor) _currentEditor = string.IsNullOrWhiteSpace(result.editorUserId) ? "none" : result.editorUserId; else _currentEditor = "none"; RepaintSceneView(); } private static async Task Request(string endpoint, object payload) where T : class { _requestInFlight = true; try { string json = JsonUtility.ToJson(payload); byte[] bytes = Encoding.UTF8.GetBytes(json); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ServerUrl + endpoint); request.Method = "POST"; request.ContentType = "application/json"; request.Timeout = 5000; request.ReadWriteTimeout = 5000; request.ContentLength = bytes.Length; using (var requestStream = await request.GetRequestStreamAsync()) { await requestStream.WriteAsync(bytes, 0, bytes.Length); } using var response = await request.GetResponseAsync(); using var responseStream = response.GetResponseStream(); using var reader = new StreamReader(responseStream); string responseJson = await reader.ReadToEndAsync(); if (string.IsNullOrWhiteSpace(responseJson)) return null; if (!responseJson.TrimStart().StartsWith("{")) { Debug.LogError($"[ScenePresence] Non-JSON response from {endpoint}: {responseJson}"); return null; } //Debug.Log($"[ScenePresence] {endpoint} => {responseJson}"); return JsonUtility.FromJson(responseJson); } catch (Exception ex) { Debug.LogError($"[ScenePresence] Request failed ({endpoint}): {ex}"); return null; } finally { _requestInFlight = false; } } private static void SetOffline() { _offline = true; RepaintSceneView(); } private static void RepaintSceneView() { if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.Repaint(); } private static string LoadOrAskUserId() { try { if (File.Exists(ConfigPath)) { UserConfig saved = JsonUtility.FromJson(File.ReadAllText(ConfigPath)); if (saved != null && !string.IsNullOrWhiteSpace(saved.userId)) return saved.userId.Trim(); } } catch (Exception ex) { Debug.LogWarning($"[ScenePresence] Failed to read config: {ex.Message}"); } EditorApplication.delayCall += () => { AskUserIdWindow.ShowWindow(SaveUserIdAndRefresh); }; return null; } private static void SaveUserIdAndRefresh(string userId) { if (string.IsNullOrWhiteSpace(userId)) return; _userId = userId.Trim(); SaveUserId(_userId); EditorApplication.delayCall += async () => { Scene scene = SceneManager.GetActiveScene(); if (!string.IsNullOrEmpty(scene.path)) { _scenePath = scene.path; await OpenScene(); } RepaintSceneView(); }; } private static void SaveUserId(string userId) { try { string directory = Path.GetDirectoryName(ConfigPath); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); UserConfig config = new UserConfig { userId = userId }; File.WriteAllText(ConfigPath, JsonUtility.ToJson(config, true)); } catch (Exception ex) { Debug.LogError($"[ScenePresence] Failed to save config: {ex.Message}"); } } [Serializable] private class UserConfig { public string userId; } [Serializable] private class BaseRequest { public string projectName; public string scenePath; public string userId; public string dateTime; } [Serializable] private class EditModeRequest : BaseRequest { public bool force; } [Serializable] private class OpenSceneResponse { public bool success; public bool hasEditor; public string editorUserId; public string editorDateTime; public string editorServerTakenAt; public bool editorOnline; } [Serializable] private class HeartbeatResponse { public bool success; public bool hasEditor; public string editorUserId; public bool editorOnline; } [Serializable] private class SetEditModeResponse { public bool success; public bool granted; public bool forced; public string occupiedBy; public string editorUserId; } [Serializable] private class ReleaseResponse { public bool success; public bool released; } private enum UiState { MissingUserId, Free, OwnedByOther, OwnedByMe, Offline } } public class AskUserIdWindow : EditorWindow { private static Action _onSave; private static AskUserIdWindow _instance; private string _userId = ""; public static void ShowWindow(Action onSave) { if (_instance != null) { _instance.Focus(); return; } _onSave = onSave; _instance = CreateInstance(); _instance.titleContent = new GUIContent("Scene Presence Setup"); _instance.minSize = new Vector2(240, 60); _instance.maxSize = new Vector2(240, 60); _instance.ShowUtility(); _instance.Focus(); } private void OnDestroy() { if (_instance == this) _instance = null; } private void OnGUI() { GUILayout.Space(10); EditorGUIUtility.labelWidth = 60; _userId = EditorGUILayout.TextField("User ID", _userId); GUILayout.FlexibleSpace(); using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); GUI.enabled = !string.IsNullOrWhiteSpace(_userId); if (GUILayout.Button("Save", GUILayout.Width(80))) { _onSave?.Invoke(_userId.Trim()); Close(); } GUI.enabled = true; } GUILayout.Space(10); } }