DIFF

PluginUndergroundTriggerManager.c

#ifdef DIAG_DEVELOPER
class PluginUndergroundTriggerManager : PluginBase
{
	private static PluginUndergroundTriggerManager m_Instance;

	private static const float RAYCAST_LENGTH 	= 1000.0;
	private static const string DATETIME_FORMAT = "%1%2%3T%4%5%6Z";
	private static const float SORT_RESORT_THRESHOLD = 10.0;
	private static const int MAX_DEBUG_DRAW_COUNT = 100;
	private static const float MAX_DEBUG_DRAW_DISTANCE = 300.0;

	private bool m_IsOpen;

	private vector m_LastSortCameraPosition;

	private bool m_BreadcrumbEditMode;
	private int m_BreadcrumbEditOriginalCount;
	private bool m_BreadcrumbsDirty;
	private bool m_TriggersDirty;

	private ref array<UndergroundTriggerHandle> m_TriggerHandles;
	private ref array<UndergroundBreadcrumbHandle> m_BreadcrumbHandles;

	private ref ArrayBoxDraw m_TriggerVisuals;
	private ref BoxDraw m_TriggerVisualsSelected;

	private ref ArrayBreadcrumbDraw m_BreadcrumbVisuals;
	private ref BreadcrumbDraw m_BreadcrumbVisualsSelected;

	private ref ScriptInvoker m_OnUpdateData;

	private ref ScriptInvoker m_OnSelectTrigger;
	private ref ScriptInvoker m_OnDeleteTrigger;

	private ref ScriptInvoker m_OnSelectBreadcrumb;
	private ref ScriptInvoker m_OnDeleteBreadcrumb;

	private ref ScriptInvoker m_OnBreadcrumbEditModeChanged;

	void PluginUndergroundTriggerManager()
	{
		if (m_Instance == null)
			m_Instance = this;
	}
	
	// --------------------------------------------------------------------------------

	static PluginUndergroundTriggerManager GetInstance()
	{
		return PluginUndergroundTriggerManager.Cast(GetPlugin(PluginUndergroundTriggerManager));
	}

	// --------------------------------------------------------------------------------

	ScriptInvoker Event_OnUpdateData()
	{
		if (!m_OnUpdateData)
			m_OnUpdateData = new ScriptInvoker();

		return m_OnUpdateData;
	}

	// --------------------------------------------------------------------------------

	ScriptInvoker Event_OnSelectTrigger()
	{
		if (!m_OnSelectTrigger)
			m_OnSelectTrigger = new ScriptInvoker();

		return m_OnSelectTrigger;
	}
	
	// --------------------------------------------------------------------------------

	ScriptInvoker Event_OnDeleteTrigger()
	{
		if (!m_OnDeleteTrigger)
			m_OnDeleteTrigger = new ScriptInvoker();

		return m_OnDeleteTrigger;
	}
	
	// --------------------------------------------------------------------------------

	ScriptInvoker Event_OnSelectBreadcrumb()
	{
		if (!m_OnSelectBreadcrumb)
			m_OnSelectBreadcrumb = new ScriptInvoker();

		return m_OnSelectBreadcrumb;
	}
	
	// --------------------------------------------------------------------------------

	ScriptInvoker Event_OnDeleteBreadcrumb()
	{
		if (!m_OnDeleteBreadcrumb)
			m_OnDeleteBreadcrumb = new ScriptInvoker();

		return m_OnDeleteBreadcrumb;
	}
	
	// --------------------------------------------------------------------------------

	ScriptInvoker Event_OnBreadcrumbEditModeChanged()
	{
		if (!m_OnBreadcrumbEditModeChanged)
			m_OnBreadcrumbEditModeChanged = new ScriptInvoker();

		return m_OnBreadcrumbEditModeChanged;
	}

	// --------------------------------------------------------------------------------

	override void OnUpdate(float delta_time)
	{
		if (m_IsOpen)
		{
			vector cameraPos = FreeDebugCamera.GetInstance().GetPosition();
			
			if (vector.Distance(cameraPos, m_LastSortCameraPosition) > SORT_RESORT_THRESHOLD)
			{
				m_LastSortCameraPosition = cameraPos;
				
				if (m_TriggerVisuals.Count() > 1)
					SortBoxDrawsByDistance(m_TriggerVisuals);
				
				if (m_BreadcrumbVisuals.Count() > 1)
					SortBreadcrumbDrawsByDistance(m_BreadcrumbVisuals);
			}
			
			UpdateTriggers(cameraPos);
			UpdateBreadcrumbs(cameraPos);
		}
	}
	
	private void UpdateTriggers(vector cameraPos)
	{
		int limit = Math.Min(m_TriggerVisuals.Count(), MAX_DEBUG_DRAW_COUNT);
		bool selectedWasDrawn = false;
		
		for (int i = 0; i < limit; ++i)
		{
			BoxDraw triggerVisual = m_TriggerVisuals.Get(i);
			
			if (vector.Distance(cameraPos, triggerVisual.GetCenter()) > MAX_DEBUG_DRAW_DISTANCE)
				break;
			
			if (triggerVisual == m_TriggerVisualsSelected)
				selectedWasDrawn = true;
			
			triggerVisual.SetAllowTransform(triggerVisual == m_TriggerVisualsSelected && !m_BreadcrumbEditMode);
			triggerVisual.Update();
		}
		
		if (m_TriggerVisualsSelected && !selectedWasDrawn)
		{
			m_TriggerVisualsSelected.SetAllowTransform(!m_BreadcrumbEditMode);
			m_TriggerVisualsSelected.Update();
		}
	}
	
	private void UpdateBreadcrumbs(vector cameraPos)
	{
		int limit = Math.Min(m_BreadcrumbVisuals.Count(), MAX_DEBUG_DRAW_COUNT);
		bool selectedWasDrawn = false;
		
		for (int i = 0; i < limit; ++i)
		{
			BreadcrumbDraw breadCrumbVisual = m_BreadcrumbVisuals.Get(i);
			
			if (vector.Distance(cameraPos, breadCrumbVisual.GetCenter()) > MAX_DEBUG_DRAW_DISTANCE)
				break;
			
			if (breadCrumbVisual == m_BreadcrumbVisualsSelected)
				selectedWasDrawn = true;
			
			if (m_BreadcrumbEditMode)
				breadCrumbVisual.SetAllowTransform(breadCrumbVisual == m_BreadcrumbVisualsSelected);
			else
				breadCrumbVisual.SetAllowTransform(false);
			breadCrumbVisual.Update();
		}
		
		if (m_BreadcrumbVisualsSelected && !selectedWasDrawn)
		{
			m_BreadcrumbVisualsSelected.SetAllowTransform(true);
			m_BreadcrumbVisualsSelected.Update();
		}
	}
	
	// --------------------------------------------------------------------------------

	override void OnInit()
	{
		m_TriggerHandles 	= new array<UndergroundTriggerHandle>();
		m_BreadcrumbHandles = new array<UndergroundBreadcrumbHandle>();
		m_TriggerVisuals 	= new ArrayBoxDraw();
		m_BreadcrumbVisuals = new ArrayBreadcrumbDraw();

		LoadDataFromFile();
	}
	
	// --------------------------------------------------------------------------------

	void OnMouseWheel(int wheel)
	{
		if (!IsMouseInScene())
			return;

		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	void OnUIEditorOpened()
	{
		m_IsOpen = true;

		//OnInit(); // leaves the data untouched during runtime
		if (!DeveloperFreeCamera.IsFreeCameraEnabled())
			DeveloperFreeCamera.EnableFreeCameraSceneEditor(PlayerBase.Cast(g_Game.GetPlayer()));

		DiagMenu.SetEngineValue(EDiagMenuGame.ENF_DIAG_GAME_FREECAM_NOCLIP, true);
		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	void OnUIEditorClosed()
	{
		m_IsOpen = false;

		DeveloperFreeCamera.FreeCameraToggle(PlayerBase.Cast(g_Game.GetPlayer()));
		DiagMenu.SetEngineValue(EDiagMenuGame.ENF_DIAG_GAME_FREECAM_NOCLIP, false);
		Event_OnUpdateData().Invoke();
		m_TriggerVisuals.Update();
		m_BreadcrumbVisuals.Update();
	}

	// --------------------------------------------------------------------------------

	void Event_OnClick()
	{
		if (!IsMouseInScene() || !m_IsOpen)
			return;

		vector dir = g_Game.GetPointerDirection();
		vector from = FreeDebugCamera.GetInstance().GetPosition();
		vector to = from + (dir * RAYCAST_LENGTH);

		RaycastRVParams params = new RaycastRVParams(from, to, null);
		params.type = ObjIntersectGeom;

		array<ref RaycastRVResult> results = new array<ref RaycastRVResult>();
		array<Object> excluded = new array<Object>();
		if (DayZPhysics.RaycastRVProxy(params, results, excluded))
		{
			if (results.Count() == 0)
				return;

			UndergroundTriggerHandle triggerHandle;
			UndergroundBreadcrumbHandle breadcrumbHandle;
			if (Class.CastTo(triggerHandle, results[0].obj))
			{
				if (!m_BreadcrumbEditMode)
					SelectTrigger(triggerHandle);
			}
			else if (Class.CastTo(breadcrumbHandle, results[0].obj))
			{
				if (m_BreadcrumbEditMode)
					SelectBreadcrumb(breadcrumbHandle);
			}
			else
			{
				if (!m_BreadcrumbEditMode)
				{
					SelectTrigger(null);
					SelectBreadcrumb(null);
				}
				else
				{
					SelectBreadcrumb(null);
				}
			}
		}
		else
		{
			if (!m_BreadcrumbEditMode)
			{
				SelectTrigger(null);
				SelectBreadcrumb(null);
			}
			else
			{
				SelectBreadcrumb(null);
			}
		}

		Event_OnUpdateData().Invoke();
	}

	// --------------------------------------------------------------------------------

	void Event_OnDoubleClick()
	{
		if (!IsMouseInScene() || !m_IsOpen)
			return;

		vector dir = g_Game.GetPointerDirection();
		vector from = FreeDebugCamera.GetInstance().GetPosition();
		vector to = from + (dir * RAYCAST_LENGTH);

		RaycastRVParams params = new RaycastRVParams(from, to, null);
		params.type = ObjIntersectGeom;

		array<ref RaycastRVResult> results = new array<ref RaycastRVResult>();
		array<Object> excluded = new array<Object>();
		if (DayZPhysics.RaycastRVProxy(params, results, excluded))
		{
			if (m_BreadcrumbEditMode)
			{
				AddBreadcrumb(results[0].pos);
			}
			else
			{
				BoxDraw box = new BoxDraw(results[0].pos);
				m_TriggerVisuals.Insert(box);
				UndergroundTriggerHandle handle = box.GetTriggerHandle();
				handle.SetHandleName(string.Format("trigger-%1", g_Game.GetTime()));
				box.Event_OnTransformChanged().Insert(MarkTriggersDirtyOnTransform);
				m_TriggerHandles.Insert(handle);

				MarkTriggersDirty();
				SelectTrigger(handle);
			}
		}

		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	array<UndergroundTriggerHandle> GetTriggersList()
	{
		return m_TriggerHandles;
	}

	// --------------------------------------------------------------------------------

	array<UndergroundBreadcrumbHandle> GetBreadcrumbsList()
	{
		return m_BreadcrumbHandles;
	}
	
	// --------------------------------------------------------------------------------

	void EditorToggle()
	{
		if (g_Game.IsMultiplayer())
			return;

		if (IsOpened())
			g_Game.GetUIManager().Back();
		else
		{
			g_Game.GetUIManager().EnterScriptedMenu(MENU_UNDERGROUND_TRIGGER_EDITOR, null);
			Event_OnUpdateData().Invoke();
		}
	}

	// --------------------------------------------------------------------------------

	bool IsOpened()
	{
		return m_IsOpen;
	}
	
	// --------------------------------------------------------------------------------

	bool IsBreadcrumbEditMode()
	{
		return m_BreadcrumbEditMode;
	}

	// --------------------------------------------------------------------------------

	bool IsBreadcrumbsDirty()
	{
		return m_BreadcrumbsDirty;
	}

	// --------------------------------------------------------------------------------
	
	bool IsTriggersDirty()
	{
		return m_TriggersDirty;
	}
	
	// --------------------------------------------------------------------------------

	// Marks unexported trigger changes; refreshes UI only on the false->true edge
	void MarkTriggersDirty()
	{
		if (m_TriggersDirty)
			return;

		m_TriggersDirty = true;
		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	// ScriptInvoker-compatible hook for BoxDraw.Event_OnTransformChanged.
	// Ignores no-op re-emits (gizmo selection / init) that pass an unchanged transform.
	void MarkTriggersDirtyOnTransform(vector oldTransform[4], vector newTransform[4])
	{
		if (IsTransformUnchanged(oldTransform, newTransform))
			return;

		MarkTriggersDirty();
	}
	
	// --------------------------------------------------------------------------------

	// ScriptInvoker-compatible hook for BreadcrumbDraw.Event_OnTransformChanged.
	// Ignores no-op re-emits (gizmo selection / init) that pass an unchanged transform.
	void MarkBreadcrumbsDirtyOnTransform(vector oldTransform[4], vector newTransform[4])
	{
		if (IsTransformUnchanged(oldTransform, newTransform))
			return;

		MarkBreadcrumbsDirty();
	}
	
	// --------------------------------------------------------------------------------

	// Marks unsaved breadcrumb changes; refreshes UI only on the false->true edge
	void MarkBreadcrumbsDirty()
	{
		if (!m_BreadcrumbEditMode || m_BreadcrumbsDirty)
			return;

		m_BreadcrumbsDirty = true;
		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	void EnterBreadcrumbEditMode()
	{
		UndergroundTriggerHandle triggerHandle = GetSelectedTrigger();
		if (!triggerHandle)
			return;

		if (!triggerHandle.m_Data.Breadcrumbs)
			triggerHandle.m_Data.Breadcrumbs = new array<ref JsonUndergroundAreaBreadcrumb>();

		m_BreadcrumbEditMode = true;
		m_BreadcrumbEditOriginalCount = triggerHandle.m_Data.Breadcrumbs.Count();
		m_BreadcrumbsDirty = false;

		Event_OnBreadcrumbEditModeChanged().Invoke(true);
		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	void SaveBreadcrumbs()
	{
		UndergroundTriggerHandle triggerHandle = GetSelectedTrigger();
		if (!triggerHandle || !triggerHandle.m_Data.Breadcrumbs)
			return;

		// Sync visual positions back to the data model
		foreach (int index, UndergroundBreadcrumbHandle breadcrumbHandle : m_BreadcrumbHandles)
		{
			if (index >= triggerHandle.m_Data.Breadcrumbs.Count())
				break;

			vector pos = breadcrumbHandle.m_Reference.GetCenter();
			JsonUndergroundAreaBreadcrumb breadcrumbData = triggerHandle.m_Data.Breadcrumbs[index];
			breadcrumbData.Position = new TFloatArray();
			breadcrumbData.Position.Insert(pos[0]);
			breadcrumbData.Position.Insert(pos[1]);
			breadcrumbData.Position.Insert(pos[2]);
			
			breadcrumbData.EyeAccommodation = breadcrumbHandle.m_Data.EyeAccommodation;
			breadcrumbData.Radius			= breadcrumbHandle.m_Data.Radius;
			breadcrumbData.LightLerp 		= breadcrumbHandle.m_Data.LightLerp;
			breadcrumbData.UseRaycast 		= breadcrumbHandle.m_Data.UseRaycast;
		}

		// Advance snapshot so Cancel from this point only undoes post-save changes
		m_BreadcrumbEditOriginalCount = triggerHandle.m_Data.Breadcrumbs.Count();
		m_BreadcrumbsDirty = false;
		m_TriggersDirty = true;
		Event_OnUpdateData().Invoke();
	}

	// --------------------------------------------------------------------------------

	void ExitBreadcrumbEditMode(bool save)
	{
		UndergroundTriggerHandle triggerHandle = GetSelectedTrigger();

		m_BreadcrumbEditMode = false;
		m_BreadcrumbsDirty = false;

		if (!save && triggerHandle && triggerHandle.m_Data.Breadcrumbs)
		{
			// Remove breadcrumbs added since last save from the data model
			for (int i = triggerHandle.m_Data.Breadcrumbs.Count() - 1; i >= m_BreadcrumbEditOriginalCount; i--)
				triggerHandle.m_Data.Breadcrumbs.Remove(i);

			// Re-select trigger to reload breadcrumb visuals from the unmodified m_Data
			SelectTrigger(triggerHandle);
		}

		if (m_BreadcrumbVisualsSelected)
		{
			m_BreadcrumbVisualsSelected.SetAllowTransform(false);
			m_BreadcrumbVisualsSelected = null;
		}

		Event_OnBreadcrumbEditModeChanged().Invoke(false);
		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	void SelectTrigger(UndergroundTriggerHandle handle)
	{
		if (m_BreadcrumbEditMode)
			return;

		if (m_TriggerVisualsSelected)
		{
			m_TriggerVisualsSelected.SetAllowTransform(false);
			m_TriggerVisualsSelected = null;
			DeleteAllBreadcrumbs();
		}

		GetGizmoApi().DeselectAll();

		if (handle)
		{
			if (m_TriggerHandles.Find(handle) > INDEX_NOT_FOUND)
			{
				m_TriggerVisualsSelected = handle.m_Reference;
				m_TriggerVisualsSelected.SetAllowTransform(true);
				m_TriggerVisualsSelected.Select();

				Event_OnSelectTrigger().Invoke(handle);
				Event_OnUpdateData().Invoke();

				if (handle.m_Data.Breadcrumbs && handle.m_Data.Breadcrumbs.Count() > 0)
				{
					m_BreadcrumbVisuals = new ArrayBreadcrumbDraw();
					foreach (int index, JsonUndergroundAreaBreadcrumb breadcrumbData : handle.m_Data.Breadcrumbs)
					{
						BreadcrumbDraw breadcrumb = new BreadcrumbDraw(breadcrumbData.GetPosition());
						UndergroundBreadcrumbHandle breadcrumbHandle = breadcrumb.GetBreadcrumbHandle();

						handle.m_Reference.Event_OnTransformChanged().Insert(breadcrumbHandle.m_Reference.ApplyTransformation);
						breadcrumb.Event_OnTransformChanged().Insert(MarkBreadcrumbsDirtyOnTransform);

						breadcrumbHandle.m_ParentTrigger = handle;
						if (!breadcrumbData.Comment)
						{
							breadcrumbHandle.SetHandleName(string.Format("Breadcrumb #%1", index));
							m_BreadcrumbHandles.Insert(breadcrumbHandle);
							
							breadcrumbHandle.m_Data.EyeAccommodation = breadcrumbData.EyeAccommodation;
							breadcrumbHandle.m_Data.Radius = breadcrumbData.Radius;
							breadcrumbHandle.m_Data.LightLerp = breadcrumbData.LightLerp;
							breadcrumbHandle.m_Data.UseRaycast = breadcrumbData.UseRaycast;
						}

						m_BreadcrumbVisuals.Insert(breadcrumb);
					}
				}
			}
		}
		else
		{
			Event_OnSelectTrigger().Invoke(null);
			Event_OnUpdateData().Invoke();
		}
	}
	
	// --------------------------------------------------------------------------------

	void SelectBreadcrumb(UndergroundBreadcrumbHandle handle)
	{
		if (m_BreadcrumbVisualsSelected)
		{
			m_BreadcrumbVisualsSelected.SetAllowTransform(false);
			m_BreadcrumbVisualsSelected = null;
		}

		GetGizmoApi().DeselectAll();

		if (handle)
		{
			if (m_BreadcrumbHandles.Find(handle) > INDEX_NOT_FOUND)
			{
				m_BreadcrumbVisualsSelected = handle.m_Reference;
				m_BreadcrumbVisualsSelected.SetAllowTransform(true);
				m_BreadcrumbVisualsSelected.Select();

				Event_OnSelectBreadcrumb().Invoke(handle);
				Event_OnUpdateData().Invoke();
			}
		}
		else
		{
			Event_OnSelectBreadcrumb().Invoke(null);
			Event_OnUpdateData().Invoke();
		}
	}
	
	// --------------------------------------------------------------------------------

	UndergroundTriggerHandle GetSelectedTrigger()
	{
		if (m_TriggerVisualsSelected)
			return m_TriggerVisualsSelected.GetTriggerHandle();

		return null;
	}
	
	// --------------------------------------------------------------------------------

	UndergroundBreadcrumbHandle GetSelectedBreadcrumb()
	{
		if (m_BreadcrumbVisualsSelected)
			return m_BreadcrumbVisualsSelected.GetBreadcrumbHandle();

		return null;
	}
	
	// --------------------------------------------------------------------------------

	void AddBreadcrumb(vector position)
	{
		UndergroundTriggerHandle triggerHandle = GetSelectedTrigger();
		if (!triggerHandle || !triggerHandle.m_Data)
			return;

		if (!triggerHandle.m_Data.Breadcrumbs)
			triggerHandle.m_Data.Breadcrumbs = new array<ref JsonUndergroundAreaBreadcrumb>();

		// Add to data model
		JsonUndergroundAreaBreadcrumb breadcrumbData = new JsonUndergroundAreaBreadcrumb();
		breadcrumbData.Position = new TFloatArray();
		breadcrumbData.Position.Insert(position[0]);
		breadcrumbData.Position.Insert(position[1]);
		breadcrumbData.Position.Insert(position[2]);
		triggerHandle.m_Data.Breadcrumbs.Insert(breadcrumbData);

		// Create visual
		BreadcrumbDraw breadcrumb = new BreadcrumbDraw(position);
		UndergroundBreadcrumbHandle breadcrumbHandle = breadcrumb.GetBreadcrumbHandle();

		triggerHandle.m_Reference.Event_OnTransformChanged().Insert(breadcrumbHandle.m_Reference.ApplyTransformation);
		breadcrumb.Event_OnTransformChanged().Insert(MarkBreadcrumbsDirtyOnTransform);

		breadcrumbHandle.m_ParentTrigger = triggerHandle;
		breadcrumbHandle.SetHandleName(string.Format("Breadcrumb #%1", m_BreadcrumbHandles.Count()));

		m_BreadcrumbHandles.Insert(breadcrumbHandle);
		m_BreadcrumbVisuals.Insert(breadcrumb);

		m_BreadcrumbsDirty = true;

		SelectBreadcrumb(breadcrumbHandle);
		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	void DeleteBreadcrumb(UndergroundBreadcrumbHandle handle)
	{
		int index = m_BreadcrumbHandles.Find(handle);
		if (index == INDEX_NOT_FOUND)
			return;

		// Remove from parent trigger data model at the matching index
		UndergroundTriggerHandle triggerHandle = handle.m_ParentTrigger;
		if (triggerHandle && index < triggerHandle.m_Data.Breadcrumbs.Count())
			triggerHandle.m_Data.Breadcrumbs.Remove(index);

		if (m_BreadcrumbVisualsSelected == handle.m_Reference)
		{
			m_BreadcrumbVisualsSelected.SetAllowTransform(false);
			m_BreadcrumbVisualsSelected = null;
		}

		m_BreadcrumbVisuals.RemoveItem(handle.m_Reference);
		m_BreadcrumbHandles.Remove(index);

		if (m_BreadcrumbEditMode)
			m_BreadcrumbsDirty = true;

		Event_OnDeleteBreadcrumb().Invoke(handle);
		Event_OnUpdateData().Invoke();
	}
	
	// --------------------------------------------------------------------------------

	void DeleteTrigger(UndergroundTriggerHandle handle)
	{
		GetGizmoApi().DeselectAll();

		if (handle)
		{
			if (m_TriggerHandles.Find(handle) > INDEX_NOT_FOUND)
			{
				m_TriggerVisualsSelected = null;

				m_TriggerVisuals.RemoveItem(handle.m_Reference);
				m_TriggerHandles.RemoveItem(handle);

				m_TriggersDirty = true;

				Event_OnDeleteTrigger().Invoke(handle);
				DeleteAllBreadcrumbs();
				Event_OnUpdateData().Invoke();
			}
		}
	}
	
	// --------------------------------------------------------------------------------

	void DeleteAllTrigers()
	{
		int count = m_TriggerHandles.Count();
		if (count > 0)
		{
			m_TriggerVisualsSelected = null;

			m_TriggerVisuals.Clear();
			m_TriggerHandles.Clear();

			DeleteAllBreadcrumbs();

			Event_OnUpdateData().Invoke();
		}
	}
	
	// --------------------------------------------------------------------------------

	void DeleteAllBreadcrumbs()
	{
		int count = m_BreadcrumbHandles.Count();
		if (count > 0)
		{
			m_BreadcrumbVisualsSelected = null;

			m_BreadcrumbHandles.Clear();
			m_BreadcrumbVisuals.Clear();

			Event_OnUpdateData().Invoke();
		}
	}
	
	// --------------------------------------------------------------------------------

	void ExportData()
	{
		JsonUndergroundTriggers jsonData = new JsonUndergroundTriggers();
		jsonData.Triggers = new array<ref JsonUndergroundAreaTriggerData>();

		foreach (UndergroundTriggerHandle triggerHandle : m_TriggerHandles)
		{
			vector size = triggerHandle.m_Reference.GetSize();
			triggerHandle.m_Data.Size = new TFloatArray();
			triggerHandle.m_Data.Size.Insert(size[0]);
			triggerHandle.m_Data.Size.Insert(size[1]);
			triggerHandle.m_Data.Size.Insert(size[2]);

			vector center = triggerHandle.m_Reference.GetCenter();
			triggerHandle.m_Data.Position = new TFloatArray();
			triggerHandle.m_Data.Position.Insert(center[0]);
			triggerHandle.m_Data.Position.Insert(center[1]);
			triggerHandle.m_Data.Position.Insert(center[2]);

			vector orientation = triggerHandle.m_Reference.GetOrientation();
			triggerHandle.m_Data.Orientation = new TFloatArray();
			triggerHandle.m_Data.Orientation.Insert(orientation[0]);
			triggerHandle.m_Data.Orientation.Insert(orientation[1]);
			triggerHandle.m_Data.Orientation.Insert(orientation[2]);
			
			foreach (int index, UndergroundBreadcrumbHandle breadcrumbHandle : m_BreadcrumbHandles)
			{
				if (breadcrumbHandle.m_ParentTrigger && triggerHandle != breadcrumbHandle.m_ParentTrigger)
					continue;

				vector breadcrumbPosition = breadcrumbHandle.m_Reference.GetCenter();
				breadcrumbHandle.m_Data.Position = new TFloatArray();
				breadcrumbHandle.m_Data.Position.Insert(breadcrumbPosition[0]);
				breadcrumbHandle.m_Data.Position.Insert(breadcrumbPosition[1]);
				breadcrumbHandle.m_Data.Position.Insert(breadcrumbPosition[2]);

				JsonUndergroundAreaBreadcrumb originalBreadcrumb  = JsonUndergroundAreaBreadcrumb.Cast(triggerHandle.m_Data.Breadcrumbs[index]);
				originalBreadcrumb.Position 		= breadcrumbHandle.m_Data.Position;
				originalBreadcrumb.EyeAccommodation = breadcrumbHandle.m_Data.EyeAccommodation;
				originalBreadcrumb.Radius			= breadcrumbHandle.m_Data.Radius;
				originalBreadcrumb.LightLerp 		= breadcrumbHandle.m_Data.LightLerp;
				originalBreadcrumb.UseRaycast 		= breadcrumbHandle.m_Data.UseRaycast;
			}

			JsonUndergroundAreaTriggerData triggerData = new JsonUndergroundAreaTriggerData();
			triggerData = triggerHandle.m_Data;

			jsonData.Triggers.Insert(triggerData);
		}

		string errorMessage;
		if (!JsonFileLoader<JsonUndergroundTriggers>.SaveFile("$mission:cfgundergroundtriggers.json", jsonData, errorMessage))
		{
			ErrorEx(errorMessage);
			return;
		}

		m_TriggersDirty = false;
		Event_OnUpdateData().Invoke();
		
		OnExport();
	}
	
	// --------------------------------------------------------------------------------

	void FocusOnObject()
	{
		if (m_TriggerVisualsSelected)
			FocusCameraOn(m_TriggerVisualsSelected.GetCenter(), m_TriggerVisualsSelected.GetSize());
		else if (m_BreadcrumbVisualsSelected)
			// Breadcrumbs have no real size, use an artificial 2m distance
			FocusCameraOn(m_BreadcrumbVisualsSelected.GetCenter(), Vector(1, 1, 1), 2.0);
	}
	
	// --------------------------------------------------------------------------------

	private void FocusCameraOn(vector position, vector size, float maxDistance = 0)
	{
		vector cameraPosition = position;
		cameraPosition[0] = cameraPosition[0] + size[0] * 2;
		cameraPosition[1] = cameraPosition[1] + size[1] + 0.5;
		cameraPosition[2] = cameraPosition[2] + size[2] * 2;

		// Clamp the camera offset to maxDistance while preserving the viewing angle.
		// Triggers pass the default (0) and are unaffected.
		if (maxDistance > 0)
		{
			vector offset = cameraPosition - position;
			if (offset.Length() > maxDistance)
				cameraPosition = position + (offset.Normalized() * maxDistance);
		}

		vector viewTarget = position;
		viewTarget[1] = viewTarget[1] + size[1] * 0.5;

		vector cameraDirection = viewTarget - cameraPosition;
		cameraDirection.Normalize();

		FreeDebugCamera.GetInstance().SetPosition(cameraPosition);
		FreeDebugCamera.GetInstance().SetDirection(cameraDirection);
	}
	
	// --------------------------------------------------------------------------------

	// Bound in PluginKeybings directly
	void FreeCameraControlEnable()
	{
		if (g_Game.GetUIManager().GetMenu())
			g_Game.GetUIManager().GetMenu().OnHide();
	}
	
	// --------------------------------------------------------------------------------

	// Bound in PluginKeybings directly
	void FreeCameraControlDisable()
	{
		if (g_Game.GetUIManager().GetMenu())
			g_Game.GetUIManager().GetMenu().OnShow();
	}
	
	// --------------------------------------------------------------------------------

	void LoadDataFromFile()
	{
		// preload data from cfgundergoundtriggers.json
		JsonUndergroundTriggers jsonData = UndergroundAreaLoader.GetData();
		if (jsonData && jsonData.Triggers)
		{
			foreach (int index, JsonUndergroundAreaTriggerData data : jsonData.Triggers)
			{
				BoxDraw box = new BoxDraw(data.GetPosition(), data.GetOrientation(), data.GetSize());
				m_TriggerVisuals.Insert(box);
				UndergroundTriggerHandle handle = box.GetTriggerHandle();
				handle.m_Data = data;
				if (!handle.m_Data.Comment)
					handle.m_Data.Comment = string.Format("trigger %1", index);
				box.Event_OnTransformChanged().Insert(MarkTriggersDirtyOnTransform);
				m_TriggerHandles.Insert(handle);
			}

			m_TriggersDirty = false;
			Event_OnUpdateData().Invoke();
		}
	}
	
	private void OnExport()
	{
		//backup original file
		int year, month, day, hour, minute, second;
		GetYearMonthDayUTC(year, month, day);
		GetHourMinuteSecondUTC(hour, minute, second);
		string datetimeUTC	= string.Format(DATETIME_FORMAT, year, month.ToStringLen(2), day.ToStringLen(2), hour.ToStringLen(2), minute.ToStringLen(2), second.ToStringLen(2));
		string backupFilename = string.Format("$mission:cfgundergroundtriggers.json.backup-%1", datetimeUTC);
		CopyFile("$mission:cfgundergroundtriggers.json", backupFilename);
	}

	// --------------------------------------------------------------------------------	

	private bool IsTransformUnchanged(vector oldTransform[4], vector newTransform[4])
	{
		const float eps = 0.0001;
		return vector.Distance(oldTransform[3], newTransform[3]) < eps && vector.Distance(oldTransform[0], newTransform[0]) < eps && vector.Distance(oldTransform[1], newTransform[1]) < eps && vector.Distance(oldTransform[2], newTransform[2]) < eps;
	}
	
	// --------------------------------------------------------------------------------

	private bool IsMouseInScene()
	{
		Widget widgetUnder = GetWidgetUnderCursor();

		if (widgetUnder != null && widgetUnder.GetName() == "panel_scene")
			return true;

		return false;
	}
	
	// --------------------------------------------------------------------------------
	
	private void SortBoxDrawsByDistance(ArrayBoxDraw arr)
	{
		int n = arr.Count();
		for (int i = 1; i < n; i++)
		{
			BoxDraw key = arr.Get(i);
			float keyDist = vector.Distance(m_LastSortCameraPosition, key.GetCenter());
			int j = i - 1;
			while (j >= 0 && vector.Distance(m_LastSortCameraPosition, arr.Get(j).GetCenter()) > keyDist)
			{
				arr.Set(j + 1, arr.Get(j));
				j--;
			}
			arr.Set(j + 1, key);
		}
	}
	
	// --------------------------------------------------------------------------------

	private void SortBreadcrumbDrawsByDistance(ArrayBreadcrumbDraw arr)
	{
		int n = arr.Count();
		for (int i = 1; i < n; i++)
		{
			BreadcrumbDraw key = arr.Get(i);
			float keyDist = vector.Distance(m_LastSortCameraPosition, key.GetCenter());
			int j = i - 1;
			while (j >= 0 && vector.Distance(m_LastSortCameraPosition, arr.Get(j).GetCenter()) > keyDist)
			{
				arr.Set(j + 1, arr.Get(j));
				j--;
			}
			arr.Set(j + 1, key);
		}
	}
}
#endif