diff --git a/DarkUI/DarkUI/Collections/ObservableList.cs b/DarkUI/DarkUI/Collections/ObservableList.cs index fce7f9a9e9..decfd5276c 100644 --- a/DarkUI/DarkUI/Collections/ObservableList.cs +++ b/DarkUI/DarkUI/Collections/ObservableList.cs @@ -67,6 +67,13 @@ protected virtual void Dispose(bool disposing) ItemsAdded?.Invoke(this, new ObservableListModified(list)); } + public new void Insert(int index, T item) + { + base.Insert(index, item); + + ItemsAdded?.Invoke(this, new ObservableListModified(new List { item })); + } + public new void Remove(T item) { base.Remove(item); diff --git a/DarkUI/DarkUI/Controls/DarkTreeView.cs b/DarkUI/DarkUI/Controls/DarkTreeView.cs index af11883542..51686715f9 100644 --- a/DarkUI/DarkUI/Controls/DarkTreeView.cs +++ b/DarkUI/DarkUI/Controls/DarkTreeView.cs @@ -22,6 +22,7 @@ public class DarkTreeView : DarkScrollView public event EventHandler SelectedNodesChanged; public event EventHandler AfterNodeExpand; public event EventHandler AfterNodeCollapse; + public event EventHandler NodesMoved; #endregion @@ -49,12 +50,17 @@ public class DarkTreeView : DarkScrollView private DarkTreeNode _provisionalNode; private DarkTreeNode _dropNode; + private DropPosition _dropPosition; + private int _dropIndicatorY; private bool _provisionalDragging; + private bool _mouseInClientArea; private List _dragNodes; private Point _dragPos; private readonly Color _borderColor = Colors.LightBorder; + private enum DropPosition { None, Before, After, Into } + #endregion #region Property Region @@ -154,6 +160,12 @@ public int Indent [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IComparer TreeViewNodeSorter { get; set; } + // Optional predicate to restrict which nodes can act as drop targets. + // When null, any node may receive drops. + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Func CanDropIntoNode { get; set; } + #endregion #region Constructor Region @@ -182,6 +194,7 @@ protected override void Dispose(bool disposing) SelectedNodesChanged = null; AfterNodeExpand = null; AfterNodeCollapse = null; + NodesMoved = null; _nodes?.Dispose(); @@ -201,6 +214,7 @@ private void Nodes_ItemsAdded(object sender, ObservableListModified e) { foreach (var node in e.Items) - { - node.ParentTree = this; - node.IsRoot = true; - - HookNodeEvents(node); - } + UnhookNodeEvents(node); UpdateNodes(); } @@ -283,25 +292,10 @@ protected override void OnMouseMove(MouseEventArgs e) } } - if (IsDragging) - { - if (_dropNode != null) - { - var rect = GetNodeFullRowArea(_dropNode); - if (!rect.Contains(OffsetMousePosition)) - { - _dropNode = null; - Invalidate(); - } - } - } - CheckHover(); if (IsDragging) - { HandleDrag(); - } base.OnMouseMove(e); } @@ -579,8 +573,10 @@ private void UpdateNodeBounds(DarkTreeNode node, int yOffset, int indent) if (ShowIcons) node.IconArea = new Rectangle(node.ExpandArea.Right + 2, iconTop, IconSize, IconSize); - else + else if (node.Nodes.Count > 0) node.IconArea = new Rectangle(node.ExpandArea.Right, iconTop, 0, 0); + else + node.IconArea = new Rectangle(node.ExpandArea.Left, iconTop, 0, 0); using (var g = CreateGraphics()) { @@ -644,7 +640,9 @@ private void DisposeIcons() private void CheckHover() { - if (!ClientRectangle.Contains(PointToClient(MousePosition))) + _mouseInClientArea = ClientRectangle.Contains(PointToClient(MousePosition)); + + if (!_mouseInClientArea) { if (IsDragging && _dropNode != null) { @@ -671,21 +669,7 @@ private void NodeMouseLeave(DarkTreeNode node) private void CheckNodeHover(DarkTreeNode node, Point location) { - if (IsDragging) - { - var rect = GetNodeFullRowArea(node); - if (rect.Contains(OffsetMousePosition)) - { - var newDropNode = _dragNodes.Contains(node) ? null : node; - - if (_dropNode != newDropNode) - { - _dropNode = newDropNode; - Invalidate(); - } - } - } - else + if (!IsDragging) { var hot = node.ExpandArea.Contains(location); if (node.ExpandAreaHot != hot) @@ -695,8 +679,11 @@ private void CheckNodeHover(DarkTreeNode node, Point location) } } - foreach (var childNode in node.Nodes) - CheckNodeHover(childNode, location); + if (node.Expanded) + { + foreach (var childNode in node.Nodes) + CheckNodeHover(childNode, location); + } } public void ExpandAllNodes() @@ -1087,26 +1074,115 @@ private void HandleDrag() if (!AllowMoveNodes) return; - var dropNode = _dropNode; + _mouseInClientArea = ClientRectangle.Contains(PointToClient(MousePosition)); - if (dropNode == null) + if (!_mouseInClientArea) { if (Cursor != Cursors.No) Cursor = Cursors.No; + ClearDropTarget(); return; } - if (!CanMoveNodes(_dragNodes, dropNode)) + // Find the node the mouse is hovering over and compute drop position. + DarkTreeNode hitNode = null; + bool hitDragNode = false; + var position = DropPosition.None; + int indicatorY = 0; + + var allVisible = GetAllNodes(); + foreach (var node in allVisible) { - if (Cursor != Cursors.No) - Cursor = Cursors.No; + var rect = GetNodeFullRowArea(node); + if (!rect.Contains(OffsetMousePosition)) + continue; + + if (_dragNodes.Contains(node)) + { + hitDragNode = true; + break; + } + + bool canDropInto = CanDropIntoNode == null || CanDropIntoNode(node); + int relativeY = OffsetMousePosition.Y - rect.Top; + int zoneSize = rect.Height / 4; + + if (relativeY < zoneSize) + { + position = DropPosition.Before; + indicatorY = rect.Top; + } + else if (relativeY > rect.Height - zoneSize) + { + position = DropPosition.After; + indicatorY = rect.Bottom; + } + else if (canDropInto) + { + position = DropPosition.Into; + indicatorY = 0; + } + else + { + // Not a valid drop-into target, treat center as before/after + // based on which half the mouse sits in. + if (relativeY < rect.Height / 2) + { + position = DropPosition.Before; + indicatorY = rect.Top; + } + else + { + position = DropPosition.After; + indicatorY = rect.Bottom; + } + } + + hitNode = node; + break; + } + + // No valid drop target hit — determine fallback based on mouse position. + if (hitNode == null && !hitDragNode && _mouseInClientArea) + { + var firstNonDrag = allVisible.FirstOrDefault(n => !_dragNodes.Contains(n)); + if (firstNonDrag != null) + { + var firstRect = GetNodeFullRowArea(firstNonDrag); + if (OffsetMousePosition.Y <= firstRect.Top) + { + // Mouse is above the first node — insert before it. + hitNode = firstNonDrag; + position = DropPosition.Before; + indicatorY = firstRect.Top; + } + else + { + // Mouse is below all nodes — append to root. + position = DropPosition.After; + indicatorY = ContentSize.Height; + } + } + } + // Validate drop target. + if (hitNode != null && !CanMoveNodes(_dragNodes, hitNode, position)) + { + ClearDropTarget(); + Cursor = Cursors.No; return; } - if (Cursor != Cursors.SizeAll) - Cursor = Cursors.SizeAll; + if (_dropNode != hitNode || _dropPosition != position || _dropIndicatorY != indicatorY) + { + _dropNode = hitNode; + _dropPosition = position; + _dropIndicatorY = indicatorY; + Invalidate(); + } + + Cursor = Cursors.SizeAll; } private void HandleDrop() @@ -1115,44 +1191,102 @@ private void HandleDrop() return; var dropNode = _dropNode; + var position = _dropPosition; - if (dropNode == null) + // Root-level drop: mouse in control area but not over any specific node. + if (dropNode == null && _mouseInClientArea) { + MoveNodesToCollection(_dragNodes, Nodes, -1); StopDrag(); + UpdateNodes(); return; } - if (CanMoveNodes(_dragNodes, dropNode, true)) + if (dropNode == null || position == DropPosition.None) { - var cachedSelectedNodes = SelectedNodes.ToList(); + StopDrag(); + return; + } - foreach (var node in _dragNodes) - { - if (node.ParentNode == null) - Nodes.Remove(node); - else - node.ParentNode.Nodes.Remove(node); + switch (position) + { + case DropPosition.Into: + MoveNodesToCollection(_dragNodes, dropNode.Nodes, -1); + dropNode.Expanded = true; + break; - dropNode.Nodes.Add(node); + case DropPosition.Before: + case DropPosition.After: + var targetCollection = dropNode.ParentNode != null ? dropNode.ParentNode.Nodes : Nodes; + int targetIndex = targetCollection.IndexOf(dropNode); + + if (position == DropPosition.After) + targetIndex++; + + MoveNodesToCollection(_dragNodes, targetCollection, targetIndex); + break; + } + + StopDrag(); + UpdateNodes(); + } + + private void MoveNodesToCollection(List nodes, ObservableList target, int insertIndex) + { + var cachedSelectedNodes = SelectedNodes.ToList(); + + foreach (var node in nodes) + { + var sourceCollection = node.ParentNode != null ? node.ParentNode.Nodes : Nodes; + + // Adjust insert index when removing from the same collection before the target position. + if (sourceCollection == target && insertIndex >= 0) + { + int currentIndex = sourceCollection.IndexOf(node); + if (currentIndex >= 0 && currentIndex < insertIndex) + insertIndex--; } - if (TreeViewNodeSorter != null) - dropNode.Nodes.Sort(TreeViewNodeSorter); + sourceCollection.Remove(node); - dropNode.Expanded = true; + if (insertIndex >= 0 && insertIndex <= target.Count) + target.Insert(insertIndex, node); + else + target.Add(node); - foreach (var node in cachedSelectedNodes) + if (insertIndex >= 0) + insertIndex++; + } + + if (TreeViewNodeSorter != null) + target.Sort(TreeViewNodeSorter); + + foreach (var node in cachedSelectedNodes) + { + if (!SelectedNodes.Contains(node)) SelectedNodes.Add(node); } - StopDrag(); - UpdateNodes(); + NodesMoved?.Invoke(this, EventArgs.Empty); + } + + private void ClearDropTarget() + { + if (_dropNode != null || _dropPosition != DropPosition.None) + { + _dropNode = null; + _dropPosition = DropPosition.None; + _dropIndicatorY = 0; + Invalidate(); + } } protected override void StopDrag() { _dragNodes = null; _dropNode = null; + _dropPosition = DropPosition.None; + _dropIndicatorY = 0; Cursor = Cursors.Default; @@ -1161,40 +1295,44 @@ protected override void StopDrag() base.StopDrag(); } - private bool CanMoveNodes(IEnumerable dragNodes, DarkTreeNode dropNode, bool isMoving = false) + private bool CanMoveNodes(IEnumerable dragNodes, DarkTreeNode dropNode, DropPosition position) { if (dropNode == null) return false; foreach (var node in dragNodes) { + // Cannot drop node onto itself. if (node == dropNode) - { - if (isMoving) - DarkMessageBox.Show(this, $"Cannot move {node.Text}. The destination folder is the same as the source folder.,", Application.ProductName, MessageBoxIcon.Error); - return false; - } - if (node.ParentNode != null && node.ParentNode == dropNode) + // For Before/After: check if the node is already at that exact position. + if (position == DropPosition.Before || position == DropPosition.After) { - if (isMoving) - DarkMessageBox.Show(this, $"Cannot move {node.Text}. The destination folder is the same as the source folder.", Application.ProductName, MessageBoxIcon.Error); + var targetCollection = dropNode.ParentNode != null ? dropNode.ParentNode.Nodes : Nodes; + var sourceCollection = node.ParentNode != null ? node.ParentNode.Nodes : Nodes; - return false; + if (sourceCollection == targetCollection) + { + int srcIdx = sourceCollection.IndexOf(node); + int dstIdx = targetCollection.IndexOf(dropNode); + if (position == DropPosition.Before && srcIdx == dstIdx - 1) + return false; + if (position == DropPosition.After && srcIdx == dstIdx + 1) + return false; + } } + // For Into: cannot drop into current parent. + if (position == DropPosition.Into && node.ParentNode == dropNode) + return false; + + // Cannot drop into a descendant of the dragged node. var parentNode = dropNode.ParentNode; while (parentNode != null) { if (node == parentNode) - { - if (isMoving) - DarkMessageBox.Show(this, $"Cannot move {node.Text}. The destination folder is a subfolder of the source folder.", Application.ProductName, MessageBoxIcon.Error); - return false; - } - parentNode = parentNode.ParentNode; } } @@ -1219,8 +1357,18 @@ protected override void OnPaint(PaintEventArgs e) protected override void PaintContent(Graphics g) { foreach (var node in Nodes) - { DrawNode(node, g); + + // Draw drop indicator line during drag operation. + if (IsDragging && _dropPosition != DropPosition.Into && _dropPosition != DropPosition.None) + { + int lineY = Math.Max(2, _dropIndicatorY); + int width = Math.Max(ContentSize.Width, Viewport.Width); + + using (var pen = new Pen(Colors.BlueHighlight, 2.0f)) + { + g.DrawLine(pen, 0, lineY, width, lineY); + } } } @@ -1246,7 +1394,7 @@ private void DrawNode(DarkTreeNode node, Graphics g) if (SelectedNodes.Count > 0 && SelectedNodes.Contains(node)) bgColor = Focused ? Colors.BlueSelection : Colors.GreySelection; - if (IsDragging && _dropNode == node) + if (IsDragging && _dropNode == node && _dropPosition == DropPosition.Into) bgColor = Focused ? Colors.BlueSelection : Colors.GreySelection; using (var b = new SolidBrush(bgColor)) diff --git a/Installer/Changes.txt b/Installer/Changes.txt index 12d8ce5231..12901eae32 100644 --- a/Installer/Changes.txt +++ b/Installer/Changes.txt @@ -6,6 +6,8 @@ Tomb Editor: * Added Content Browser tool window for more streamlined object management. * Added Flyby Timeline control to edit and preview flyby sequences. * Added "Preview flyby sequence" and "Preview camera" context menus for cameras. + * Added ability to group event sets into categories. + * Added indication of the nodes not supported by the current event type. * Added On Pickup, On Vehicle Enter and On Vehicle Exit global events in the global event set editor. * Added multiple window layout support. diff --git a/TombEditor/Controls/TriggerManager.cs b/TombEditor/Controls/TriggerManager.cs index 5205948e2c..2712f7ca84 100644 --- a/TombEditor/Controls/TriggerManager.cs +++ b/TombEditor/Controls/TriggerManager.cs @@ -24,6 +24,13 @@ public TriggerManager() InitializeComponent(); } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EventType EventType + { + get { return nodeEditor.CurrentEventType; } + set { nodeEditor.CurrentEventType = value; } + } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Event Event { diff --git a/TombEditor/Forms/FormEventSetEditor.Designer.cs b/TombEditor/Forms/FormEventSetEditor.Designer.cs index 240049a1a0..cabeb370bf 100644 --- a/TombEditor/Forms/FormEventSetEditor.Designer.cs +++ b/TombEditor/Forms/FormEventSetEditor.Designer.cs @@ -27,12 +27,13 @@ private void InitializeComponent() cbActivatorStatics = new DarkCheckBox(); cbActivatorFlyBy = new DarkCheckBox(); panelList = new DarkSectionPanel(); - dgvEvents = new DarkDataGridView(); + treeEvents = new DarkTreeView(); darkPanel1 = new DarkPanel(); butSearch = new DarkButton(); butUnassignEventSet = new DarkButton(); butDeleteEventSet = new DarkButton(); butCloneEventSet = new DarkButton(); + butNewFolder = new DarkButton(); butNewEventSet = new DarkButton(); triggerManager = new Controls.TriggerManager(); lblActivators = new DarkLabel(); @@ -47,7 +48,6 @@ private void InitializeComponent() splitContainer = new SplitContainer(); panelActivators = new DarkSectionPanel(); panelList.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)dgvEvents).BeginInit(); darkPanel1.SuspendLayout(); panelEditor.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit(); @@ -139,7 +139,7 @@ private void InitializeComponent() // panelList // panelList.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - panelList.Controls.Add(dgvEvents); + panelList.Controls.Add(treeEvents); panelList.Controls.Add(darkPanel1); panelList.Location = new System.Drawing.Point(3, 3); panelList.Name = "panelList"; @@ -147,27 +147,17 @@ private void InitializeComponent() panelList.Size = new System.Drawing.Size(258, 377); panelList.TabIndex = 22; // - // dgvEvents - // - dgvEvents.AllowUserToAddRows = false; - dgvEvents.AllowUserToDeleteRows = false; - dgvEvents.AllowUserToPasteCells = false; - dgvEvents.AllowUserToResizeColumns = false; - dgvEvents.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - dgvEvents.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; - dgvEvents.ColumnHeadersHeight = 4; - dgvEvents.ForegroundColor = System.Drawing.Color.FromArgb(220, 220, 220); - dgvEvents.Location = new System.Drawing.Point(4, 32); - dgvEvents.MultiSelect = false; - dgvEvents.Name = "dgvEvents"; - dgvEvents.ReadOnly = true; - dgvEvents.RowHeadersWidth = 41; - dgvEvents.Size = new System.Drawing.Size(250, 341); - dgvEvents.TabIndex = 0; - dgvEvents.UseAlternativeDragDropMethod = true; - dgvEvents.ColumnHeaderMouseClick += dgvEvents_ColumnHeaderMouseClick; - dgvEvents.SelectionChanged += dgvEvents_SelectedIndicesChanged; - dgvEvents.DragDrop += dgvEvents_DragDrop; + // treeEvents + // + treeEvents.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + treeEvents.Location = new System.Drawing.Point(4, 32); + treeEvents.MultiSelect = false; + treeEvents.Name = "treeEvents"; + treeEvents.AllowMoveNodes = true; + treeEvents.Size = new System.Drawing.Size(250, 341); + treeEvents.TabIndex = 0; + treeEvents.SelectedNodesChanged += treeEvents_SelectedNodesChanged; + treeEvents.DoubleClick += treeEvents_DoubleClick; // // darkPanel1 // @@ -175,6 +165,7 @@ private void InitializeComponent() darkPanel1.Controls.Add(butUnassignEventSet); darkPanel1.Controls.Add(butDeleteEventSet); darkPanel1.Controls.Add(butCloneEventSet); + darkPanel1.Controls.Add(butNewFolder); darkPanel1.Controls.Add(butNewEventSet); darkPanel1.Dock = DockStyle.Top; darkPanel1.Location = new System.Drawing.Point(1, 1); @@ -210,12 +201,12 @@ private void InitializeComponent() // butDeleteEventSet.Checked = false; butDeleteEventSet.Image = Properties.Resources.general_trash_16; - butDeleteEventSet.Location = new System.Drawing.Point(62, 3); + butDeleteEventSet.Location = new System.Drawing.Point(91, 3); butDeleteEventSet.Name = "butDeleteEventSet"; butDeleteEventSet.Size = new System.Drawing.Size(23, 23); butDeleteEventSet.TabIndex = 20; butDeleteEventSet.Tag = "AddNewRoom"; - toolTip.SetToolTip(butDeleteEventSet, "Delete selected event set"); + toolTip.SetToolTip(butDeleteEventSet, "Delete selected event set or folder"); butDeleteEventSet.Click += butDeleteEventSet_Click; // // butCloneEventSet @@ -223,13 +214,24 @@ private void InitializeComponent() butCloneEventSet.Checked = false; butCloneEventSet.DialogResult = DialogResult.Cancel; butCloneEventSet.Image = Properties.Resources.general_copy_16; - butCloneEventSet.Location = new System.Drawing.Point(33, 3); + butCloneEventSet.Location = new System.Drawing.Point(62, 3); butCloneEventSet.Name = "butCloneEventSet"; butCloneEventSet.Size = new System.Drawing.Size(23, 23); butCloneEventSet.TabIndex = 19; butCloneEventSet.Tag = "AddNewRoom"; toolTip.SetToolTip(butCloneEventSet, "Copy selected event set"); butCloneEventSet.Click += butCloneEventSet_Click; + // + // butNewFolder + // + butNewFolder.Checked = false; + butNewFolder.Image = Properties.Resources.general_Open_16; + butNewFolder.Location = new System.Drawing.Point(33, 3); + butNewFolder.Name = "butNewFolder"; + butNewFolder.Size = new System.Drawing.Size(23, 23); + butNewFolder.TabIndex = 27; + toolTip.SetToolTip(butNewFolder, "Add new folder"); + butNewFolder.Click += butNewFolder_Click; // // butNewEventSet // @@ -396,7 +398,6 @@ private void InitializeComponent() SizeGripStyle = SizeGripStyle.Hide; StartPosition = FormStartPosition.CenterParent; panelList.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)dgvEvents).EndInit(); darkPanel1.ResumeLayout(false); panelEditor.ResumeLayout(false); splitContainer.Panel1.ResumeLayout(false); @@ -418,11 +419,12 @@ private void InitializeComponent() private DarkUI.Controls.DarkCheckBox cbActivatorStatics; private DarkUI.Controls.DarkCheckBox cbActivatorFlyBy; private DarkSectionPanel panelList; - private DarkDataGridView dgvEvents; + private DarkTreeView treeEvents; private Controls.TriggerManager triggerManager; private DarkPanel darkPanel1; private DarkButton butDeleteEventSet; private DarkButton butCloneEventSet; + private DarkButton butNewFolder; private DarkButton butNewEventSet; private DarkLabel lblActivators; private DarkButton butUnassignEventSet; diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index 007a6363b8..f00d2414fd 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -1,7 +1,8 @@ -using DarkUI.Forms; +using DarkUI.Collections; +using DarkUI.Controls; +using DarkUI.Forms; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; @@ -14,14 +15,8 @@ namespace TombEditor.Forms { public partial class FormEventSetEditor : DarkForm { - private enum SortMode - { - None, - Ascending, - Descending - } - - private SortMode _nextSortMode = SortMode.Ascending; + private const string _folderNodeType = "Folder"; + private const string _folderSeparator = "/"; private VolumeInstance _instance; private readonly Editor _editor; @@ -45,6 +40,9 @@ private enum SortMode public bool GlobalMode => _usedList == _editor.Level.Settings.GlobalEventSets; public bool GenericMode => GlobalMode || _instance == null; + private HashSet _backupCollapsedFolders; + private HashSet _collapsedFolders => GlobalMode ? _editor.Level.Settings.CollapsedGlobalEventSetFolders : _editor.Level.Settings.CollapsedVolumeEventSetFolders; + public EventSet SelectedSet { get @@ -61,30 +59,21 @@ public EventSet SelectedSet if (_selectedSet == null) { - if (GenericMode && dgvEvents.Rows.Count > 0) - dgvEvents.Rows[0].Selected = true; - else - { - ClearSelection(); - ClearEventSetFromUI(); - } + ClearSelection(); + ClearEventSetFromUI(); } else { - for (int i = 0; i < dgvEvents.Rows.Count; i++) + var node = FindNodeByEventSet(_selectedSet); + if (node != null) { - if (dgvEvents.Rows[i].Tag == _selectedSet) - { - ClearSelection(); - dgvEvents.Rows[i].Selected = true; - - LoadEventSetIntoUI(_selectedSet); - break; - } + ClearSelection(); + treeEvents.SelectNode(node); + treeEvents.EnsureVisible(); + LoadEventSetIntoUI(_selectedSet); } } - if (!GenericMode) _instance.EventSet = _selectedSet; } @@ -94,7 +83,6 @@ public EventSet SelectedSet public FormEventSetEditor(bool global, VolumeInstance instance = null) { InitializeComponent(); - dgvEvents.Columns.Add(new DataGridViewColumn(new DataGridViewTextBoxCell()) { HeaderText = "Event sets" }); _editor = Editor.Instance; _editor.EditorEventRaised += EditorEventRaised; @@ -121,6 +109,16 @@ public FormEventSetEditor(bool global, VolumeInstance instance = null) // Populate and select event set list PopulateEventSetList(); + // Only folder nodes can receive drag-drop children. + treeEvents.CanDropIntoNode = n => IsFolderNode(n); + + // Sync folder paths when user drags nodes in the tree. + treeEvents.NodesMoved += (s, args) => SyncFoldersFromTree(); + + // Persist folder expansion state to the level file. + treeEvents.AfterNodeExpand += (s, args) => SyncCollapsedFolders(); + treeEvents.AfterNodeCollapse += (s, args) => SyncCollapsedFolders(); + // Gray out UI by default, if event set list is empty if (_usedList.Count == 0) UpdateUI(); @@ -136,9 +134,6 @@ protected override void OnShown(EventArgs e) // Resize splitter splitContainer.SplitterDistance = _editor.Configuration.Window_FormEventSetEditor_SplitterDistance; - // Select event set, if volume exists. Must be in OnShown event because of DDGV bug which reselects - // first row after DDGV is drawn for the first time. - if (!GenericMode) SelectedSet = _instance.EventSet; } @@ -157,8 +152,10 @@ protected override void Dispose(bool disposing) { _editor.EditorEventRaised -= EditorEventRaised; - if (DialogResult == DialogResult.Cancel) + if (DialogResult != DialogResult.OK) RestoreState(); + else + SyncFoldersFromTree(); _editor.EventSetsChange(); } @@ -216,11 +213,8 @@ public void UpdateVolume() public void ClearSelection() { - // HACK: Lock selection change to prevent DDGV from automatically selecting first row - // after clearing previous selection. - _lockSelectionChange = true; - dgvEvents.ClearSelection(); + treeEvents.SelectNodes(new List()); _lockSelectionChange = false; } @@ -251,13 +245,14 @@ private void SetupUI() private void UpdateUI() { bool eventSetSelected = SelectedSet != null; + bool folderSelected = treeEvents.SelectedNodes.Count > 0 && IsFolderNode(treeEvents.SelectedNodes[0]); tbName.Enabled = triggerManager.Enabled = cbEvents.Enabled = butUnassignEventSet.Enabled = - butCloneEventSet.Enabled = - butDeleteEventSet.Enabled = eventSetSelected; + butCloneEventSet.Enabled = eventSetSelected; + butDeleteEventSet.Enabled = eventSetSelected || folderSelected; cbActivatorLara.Enabled = cbActivatorNPC.Enabled = @@ -266,7 +261,7 @@ private void UpdateUI() cbActivatorFlyBy.Enabled = lblActivators.Enabled = eventSetSelected && !GlobalMode; - butSearch.Enabled = dgvEvents.Rows.Count > 0; + butSearch.Enabled = treeEvents.Nodes.Count > 0; } private void SetEventTooltip() @@ -345,10 +340,16 @@ private void BackupState() _backupEventSetList = new List(); foreach (var evtSet in _usedList) _backupEventSetList.Add(evtSet.Clone()); + + _backupCollapsedFolders = new HashSet(_collapsedFolders); } private void RestoreState() { + _collapsedFolders.Clear(); + foreach (var path in _backupCollapsedFolders) + _collapsedFolders.Add(path); + if (GlobalMode) { _editor.Level.Settings.GlobalEventSets = _backupEventSetList; @@ -388,18 +389,113 @@ private void PopulateEventSetList() { _lockSelectionChange = true; - dgvEvents.Rows.Clear(); + var collapsed = new HashSet(_collapsedFolders); + treeEvents.Nodes.Clear(); foreach (var evtSet in _usedList) { - var row = new DataGridViewRow { Tag = evtSet }; - row.Cells.Add(new DataGridViewTextBoxCell() { Value = evtSet.Name }); - dgvEvents.Rows.Add(row); + var parentCollection = GetOrCreateFolderNodes(evtSet.Folder); + parentCollection.Add(new DarkTreeNode(evtSet.Name) { Tag = evtSet }); } + foreach (var node in treeEvents.GetAllNodes().Where(n => IsFolderNode(n))) + node.Expanded = !collapsed.Contains(GetFolderPath(node)); + _lockSelectionChange = false; } + private DarkTreeNode FindFirstEventSetNode() => treeEvents.GetAllNodes().FirstOrDefault(n => n.Tag is EventSet); + private DarkTreeNode FindNodeByEventSet(EventSet evtSet) => treeEvents.GetAllNodes().FirstOrDefault(n => n.Tag == evtSet); + private bool IsFolderNode(DarkTreeNode node) => node != null && node.NodeType as string == _folderNodeType; + private bool FolderNameExists(ObservableList collection, string name, DarkTreeNode exclude = null) => + collection.Any(n => IsFolderNode(n) && n != exclude && string.Equals(n.Text, name, StringComparison.OrdinalIgnoreCase)); + + private ObservableList GetOrCreateFolderNodes(string path) + { + if (string.IsNullOrEmpty(path)) + return treeEvents.Nodes; + + var parts = path.Split(new[] { _folderSeparator }, StringSplitOptions.RemoveEmptyEntries); + var currentCollection = treeEvents.Nodes; + + foreach (var part in parts) + { + var existing = currentCollection.FirstOrDefault(n => IsFolderNode(n) && n.Text == part); + if (existing == null) + { + existing = new DarkTreeNode(part) { NodeType = _folderNodeType }; + currentCollection.Add(existing); + } + currentCollection = existing.Nodes; + } + + return currentCollection; + } + + private string GetFolderPath(DarkTreeNode node) + { + var parts = new List(); + var current = node; + + while (current != null) + { + if (IsFolderNode(current)) + parts.Insert(0, current.Text); + current = current.ParentNode; + } + + return string.Join(_folderSeparator, parts); + } + + private void SyncFoldersFromTree() + { + _usedList.Clear(); + + foreach (var node in treeEvents.GetAllNodes()) + { + if (node.Tag is EventSet evtSet) + { + evtSet.Folder = node.ParentNode != null ? GetFolderPath(node.ParentNode) : string.Empty; + _usedList.Add(evtSet); + } + } + } + + private void SyncCollapsedFolders() + { + _collapsedFolders.Clear(); + + foreach (var node in treeEvents.GetAllNodes()) + { + if (IsFolderNode(node) && !node.Expanded) + _collapsedFolders.Add(GetFolderPath(node)); + } + } + + private void RemoveEmptyFolderNodes() + { + bool removed; + do + { + removed = false; + foreach (var node in treeEvents.GetAllNodes()) + { + if (IsFolderNode(node) && node.Nodes.Count == 0) + { + var parent = node.ParentNode; + if (parent != null) + parent.Nodes.Remove(node); + else + treeEvents.Nodes.Remove(node); + + removed = true; + break; + } + } + } + while (removed); + } + private void ClearEventSetFromUI() { UpdateUI(); @@ -441,6 +537,7 @@ private void LoadEventSetIntoUI(EventSet newEventSet) } cbEvents.SelectedItem = newEventSet.LastUsedEvent; + triggerManager.EventType = newEventSet.LastUsedEvent; triggerManager.Event = newEventSet.Events[newEventSet.LastUsedEvent]; tbName.Text = newEventSet.Name; @@ -474,59 +571,47 @@ private void butCancel_Click(object sender, EventArgs e) Close(); } - private void dgvEvents_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) - { - switch (_nextSortMode) - { - case SortMode.Ascending: - dgvEvents.AllowUserToDragDropRows = false; - - dgvEvents.Sort(dgvEvents.Columns[e.ColumnIndex], ListSortDirection.Ascending); - dgvEvents.Columns[e.ColumnIndex].HeaderText += " ▲"; - _nextSortMode = SortMode.Descending; - break; - - case SortMode.Descending: - dgvEvents.AllowUserToDragDropRows = false; - - dgvEvents.Sort(dgvEvents.Columns[e.ColumnIndex], ListSortDirection.Descending); - dgvEvents.Columns[e.ColumnIndex].HeaderText = dgvEvents.Columns[e.ColumnIndex].HeaderText.TrimEnd('▲', ' '); - dgvEvents.Columns[e.ColumnIndex].HeaderText += " ▼"; - _nextSortMode = SortMode.None; - break; - - default: - dgvEvents.AllowUserToDragDropRows = true; - - object selectedEventCache = dgvEvents.SelectedRows.Count > 0 ? dgvEvents.SelectedRows[0].Tag : null; - PopulateEventSetList(); - ClearSelection(); - - if (selectedEventCache != null) - foreach (DataGridViewRow row in dgvEvents.Rows) - if (row.Tag == selectedEventCache) - row.Selected = true; - - dgvEvents.Columns[e.ColumnIndex].HeaderText = dgvEvents.Columns[e.ColumnIndex].HeaderText.TrimEnd('▼', ' '); - _nextSortMode = SortMode.Ascending; - break; - } - } - - private void dgvEvents_SelectedIndicesChanged(object sender, EventArgs e) + private void treeEvents_SelectedNodesChanged(object sender, EventArgs e) { if (_lockSelectionChange) return; - var newEventSet = dgvEvents.SelectedRows.Count == 0 ? null : dgvEvents.SelectedRows[0].Tag as EventSet; - SelectedSet = newEventSet; + if (treeEvents.SelectedNodes.Count == 0) + { + SelectedSet = null; + UpdateUI(); + return; + } + + var selectedNode = treeEvents.SelectedNodes[0]; + if (selectedNode.Tag is EventSet evtSet) + { + _selectedSet = evtSet; + LoadEventSetIntoUI(evtSet); + } + else + { + _selectedSet = null; + ClearEventSetFromUI(); + } UpdateUI(); } private void butNewEventSet_Click(object sender, EventArgs e) { - var name = "New " + _mode + " event set " + (dgvEvents.Rows.Count + 1).ToString(); + var name = "New " + _mode + " event set " + (_usedList.Count + 1).ToString(); + + // Determine folder from selected node. + var folder = string.Empty; + if (treeEvents.SelectedNodes.Count > 0) + { + var selected = treeEvents.SelectedNodes[0]; + if (IsFolderNode(selected)) + folder = GetFolderPath(selected); + else if (selected.ParentNode != null && IsFolderNode(selected.ParentNode)) + folder = GetFolderPath(selected.ParentNode); + } EventSet newSet; @@ -535,6 +620,7 @@ private void butNewEventSet_Click(object sender, EventArgs e) newSet = new GlobalEventSet() { Name = name, + Folder = folder, LastUsedEvent = Event.GlobalEventTypes[_editor.Configuration.NodeEditor_DefaultGlobalEventToEdit] }; } @@ -543,6 +629,7 @@ private void butNewEventSet_Click(object sender, EventArgs e) newSet = new VolumeEventSet() { Name = name, + Folder = folder, LastUsedEvent = Event.VolumeEventTypes[_editor.Configuration.NodeEditor_DefaultEventToEdit] }; } @@ -565,6 +652,7 @@ private void butCloneEventSet_Click(object sender, EventArgs e) var clonedSet = SelectedSet.Clone(); clonedSet.Name = SelectedSet.Name + " (copy)"; + clonedSet.Folder = SelectedSet.Folder; _usedList.Add(clonedSet); PopulateEventSetList(); @@ -573,24 +661,98 @@ private void butCloneEventSet_Click(object sender, EventArgs e) private void butDeleteEventSet_Click(object sender, EventArgs e) { - int index = dgvEvents.SelectedRows.Count == 0 ? 0 : dgvEvents.SelectedRows[0].Index; - if (index == dgvEvents.Rows.Count - 1) - index--; + if (treeEvents.SelectedNodes.Count == 0) + return; - EditorActions.DeleteEventSet(SelectedSet); - PopulateEventSetList(); + var selectedNode = treeEvents.SelectedNodes[0]; + + if (IsFolderNode(selectedNode)) + { + var eventSetsInFolder = GetAllEventSetsUnder(selectedNode).ToList(); + + if (eventSetsInFolder.Count > 0) + { + var result = DarkMessageBox.Show(this, + "Delete " + eventSetsInFolder.Count + " event set" + (eventSetsInFolder.Count > 1 ? "s" : string.Empty) + " in folder '" + selectedNode.Text + "'?", + "Delete folder", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); + + if (result == DialogResult.Cancel) + return; + + foreach (var evtSet in eventSetsInFolder) + EditorActions.DeleteEventSet(evtSet); + } + + // Remove the folder node from the tree. + if (selectedNode.ParentNode != null) + selectedNode.ParentNode.Nodes.Remove(selectedNode); + else + treeEvents.Nodes.Remove(selectedNode); + + SyncFoldersFromTree(); + SelectFirstAvailableEventSet(); + } + else if (selectedNode.Tag is EventSet) + { + var nextSet = FindAdjacentEventSet(selectedNode); + + EditorActions.DeleteEventSet(SelectedSet); + PopulateEventSetList(); - if (dgvEvents.Rows.Count > 0) + if (nextSet != null && _usedList.Contains(nextSet)) + SelectedSet = nextSet; + else + SelectFirstAvailableEventSet(); + } + } + + private void SelectFirstAvailableEventSet() + { + if (_usedList.Count > 0) { - SelectedSet = dgvEvents.Rows[index].Tag as EventSet; + var firstNode = FindFirstEventSetNode(); + if (firstNode != null) + SelectedSet = firstNode.Tag as EventSet; + else + SelectedSet = null; } else { SelectedSet = null; - UpdateUI(); } } + private EventSet FindAdjacentEventSet(DarkTreeNode current) + { + var allNodes = treeEvents.GetAllNodes(); + int index = allNodes.IndexOf(current); + + // Look forward first, then backward. + for (int i = index + 1; i < allNodes.Count; i++) + { + if (allNodes[i].Tag is EventSet evtSet) + return evtSet; + } + + for (int i = index - 1; i >= 0; i--) + { + if (allNodes[i].Tag is EventSet evtSet) + return evtSet; + } + + return null; + } + + private IEnumerable GetAllEventSetsUnder(DarkTreeNode node) + { + if (node.Tag is EventSet evtSet) + yield return evtSet; + + foreach (var child in node.Nodes) + foreach (var set in GetAllEventSetsUnder(child)) + yield return set; + } + private void butUnassignEventSet_Click(object sender, EventArgs e) { if (GenericMode) @@ -606,7 +768,7 @@ private void cbActivators_CheckedChanged(object sender, EventArgs e) private void butSearch_Click(object sender, EventArgs e) { - var searchPopUp = new PopUpSearch(dgvEvents) { ShowAboveControl = true }; + var searchPopUp = new PopUpSearch(treeEvents) { ShowAboveControl = true }; searchPopUp.Show(this); } @@ -622,6 +784,19 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) switch (keyData) { + case Keys.Delete: + case Keys.Back: + if (treeEvents.ContainsFocus) + { + butDeleteEventSet_Click(butDeleteEventSet, EventArgs.Empty); + return true; + } + else + { + triggerManager.ProcessKey(keyData); + } + break; + case (Keys.Control | Keys.C): var copiedNodes = triggerManager.CopyNodes(false); if (copiedNodes.Count > 0) @@ -658,6 +833,7 @@ private void cbEvents_SelectedIndexChanged(object sender, EventArgs e) if (!_lockUI) { SelectedSet.LastUsedEvent = (EventType)cbEvents.SelectedItem; + triggerManager.EventType = SelectedSet.LastUsedEvent; triggerManager.Event = SelectedSet.Events[SelectedSet.LastUsedEvent]; } } @@ -691,22 +867,95 @@ private void tbName_Validated(object sender, EventArgs e) } EditorActions.ReplaceEventSetNames(_usedList, SelectedSet.Name, tbName.Text); - dgvEvents.SelectedCells[0].Value = SelectedSet.Name = tbName.Text; + SelectedSet.Name = tbName.Text; + + var node = FindNodeByEventSet(SelectedSet); + if (node != null) + node.Text = tbName.Text; } - private void dgvEvents_DragDrop(object sender, DragEventArgs e) + private void butNewFolder_Click(object sender, EventArgs e) { - _usedList.Clear(); + var parentFolder = string.Empty; + + if (treeEvents.SelectedNodes.Count > 0) + { + var selected = treeEvents.SelectedNodes[0]; + if (IsFolderNode(selected)) + parentFolder = GetFolderPath(selected); + else if (selected.ParentNode != null && IsFolderNode(selected.ParentNode)) + parentFolder = GetFolderPath(selected.ParentNode); + } + + var parentCollection = GetOrCreateFolderNodes(parentFolder); + var newFolderName = PromptUniqueFolderName("New folder", "Enter folder name:", "New folder", parentCollection); + if (newFolderName == null) + return; + + var fullPath = string.IsNullOrEmpty(parentFolder) ? newFolderName : parentFolder + _folderSeparator + newFolderName; + GetOrCreateFolderNodes(fullPath); + + var newNode = treeEvents.GetAllNodes().LastOrDefault(n => IsFolderNode(n) && n.Text == newFolderName); + if (newNode != null) + { + treeEvents.SelectNode(newNode); + treeEvents.EnsureVisible(); + } + } + + private void RenameFolder(DarkTreeNode node) + { + if (!IsFolderNode(node)) + return; - foreach (DataGridViewRow row in dgvEvents.Rows) + var siblings = node.ParentNode?.Nodes ?? treeEvents.Nodes; + var newName = PromptUniqueFolderName("Rename folder", "Enter new folder name:", node.Text, siblings, node); + if (newName == null || newName == node.Text) + return; + + node.Text = newName; + SyncFoldersFromTree(); + } + + // Shows a name-input dialog in a loop until the user enters a name that doesn't conflict with + // an existing folder in the given collection, or cancels. Returns null on cancel or empty input. + private string PromptUniqueFolderName(string title, string prompt, string initialValue, ObservableList collection, DarkTreeNode exclude = null) + { + var current = initialValue; + while (true) { - if (row.Tag is not EventSet evtSet) - continue; + using (var inputBox = new FormInputBox(title, prompt, current)) + { + if (inputBox.ShowDialog(this) != DialogResult.OK) + return null; - _usedList.Add(evtSet); + var name = inputBox.Result.Trim(); + if (string.IsNullOrEmpty(name)) + return null; + + if (!FolderNameExists(collection, name, exclude)) + return name; + + DarkMessageBox.Show(this, "A folder with that name already exists. Specify a different name.", title, MessageBoxButtons.OK, MessageBoxIcon.Warning); + current = name; + } } } + private void treeEvents_DoubleClick(object sender, EventArgs e) + { + if (treeEvents.SelectedNodes.Count == 0) + return; + + var node = treeEvents.SelectedNodes[0]; + if (!IsFolderNode(node)) + return; + + // DarkTreeView already toggled Expanded via OnMouseDoubleClick, undo that. + node.Expanded = !node.Expanded; + RenameFolder(node); + } + private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) { if (Visible) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index da24befb8a..d6917769ce 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -25,6 +25,10 @@ public enum ConnectionMode public partial class NodeEditor : UserControl { + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EventType CurrentEventType { get; set; } + [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Vector2 ViewPosition { get; set; } = new Vector2(60.0f, 60.0f); @@ -885,21 +889,53 @@ private void DrawShadow(PaintEventArgs e, VisibleNodeBase node) e.Graphics.DrawImage(Properties.Resources.misc_Shadow, rect); } + private bool IsNodeUnsupported(TriggerNode node) + { + if (string.IsNullOrEmpty(node.Function)) + return false; + + var func = NodeFunctions.FirstOrDefault(f => f.Signature == node.Function); + if (func == null) + return false; + + return func.IsUnsupported(CurrentEventType); + } + private void DrawHeader(PaintEventArgs e, VisibleNodeBase node) { if (!node.Visible) return; - var size = TextRenderer.MeasureText(node.Node.Name, Font); + const float LabelOpacity = 0.5f; + + bool unsupported = IsNodeUnsupported(node.Node); + var headerText = unsupported ? "Not supported for this event type" : node.Node.Name; + int iconOffset = 0; + var size = TextRenderer.MeasureText(headerText, Font); var rect = node.ClientRectangle; rect.Height = size.Height; rect.Offset(node.Location); rect.Offset(0, -(int)(size.Height * 1.2f)); - using (var b = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.5f))) - e.Graphics.DrawString(node.Node.Name, Font, b, rect, - new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center }); + if (unsupported) + { + var matrix = new System.Drawing.Imaging.ColorMatrix { Matrix33 = LabelOpacity, Matrix22 = 0.0f }; + using (var attributes = new System.Drawing.Imaging.ImageAttributes()) + { + attributes.SetColorMatrix(matrix, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap); + + var icon = Properties.Resources.general_Warning_16; + var iconRect = new Rectangle(rect.X, rect.Y + (rect.Height - icon.Height), icon.Width, icon.Height); + e.Graphics.DrawImage(icon, iconRect, 0, 0, icon.Width, icon.Height, GraphicsUnit.Pixel, attributes); + iconOffset = icon.Width + 2; + } + } + + var textRect = new Rectangle(rect.X + iconOffset, rect.Y, rect.Width - iconOffset, rect.Height); + using (var b = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(LabelOpacity))) + e.Graphics.DrawString(headerText, Font, b, textRect, + new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center }); var condNode = node as VisibleNodeCondition; if (condNode == null) diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md index a20dcacdf6..3ff3058e60 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md @@ -40,6 +40,14 @@ Comment metadata entry reference (metadata block is indicated by a keyword which new line. - **!Section "SECTION"** - this will define where the node will be found inside Tomb Editor. + + - **!Supported "TYPE" "TYPE" "..."** - defines supported event types for a given node. Each event type should be + enclosed in quotes. If an event is not supported by a node, it will display a warning message when misplaced. + Possible event types are: **OnVolumeEnter, OnVolumeInside, OnVolumeLeave, OnLoop, OnLoadGame, OnSaveGame, + OnLevelStart, OnLevelEnd, OnUseItem, OnFreeze**. + + - **!Unsupported "TYPE" "TYPE" "..."** - defines unsupported event types for a given node. Acts in an opposite way + to `!Supported`. Possible event types are equal to `!Supported`. - **!Arguments "ARGDESC1" "ARGDESC2" "ARGDESC..."** - infinite amount of args, with **ARGDESC** parameters separated by commas as follows: diff --git a/TombLib/TombLib/LevelData/EventSet.cs b/TombLib/TombLib/LevelData/EventSet.cs index 4ec3201b41..65d8cc090a 100644 --- a/TombLib/TombLib/LevelData/EventSet.cs +++ b/TombLib/TombLib/LevelData/EventSet.cs @@ -188,6 +188,7 @@ public abstract class EventSet : ICloneable, IEquatable { public EventType LastUsedEvent; public string Name; + public string Folder = string.Empty; // Every volume's events can be reduced to these three. // If resulting volume should be one-shot trigger, we'll only use "OnEnter" event. diff --git a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs index 901ea52bc5..894d03e58b 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs @@ -94,6 +94,7 @@ internal static class Prj2Chunks /******/public static readonly ChunkId EventSet = ChunkId.FromString("TeEventSet"); /********/public static readonly ChunkId EventSetIndex = ChunkId.FromString("TeEventSetIndex"); /********/public static readonly ChunkId EventSetName = ChunkId.FromString("TeEventSetName"); + /********/public static readonly ChunkId EventSetFolder = ChunkId.FromString("TeEventSetFolder"); /********/public static readonly ChunkId EventSetLastUsedEventIndex = ChunkId.FromString("TeEventSetLUEI"); /********/public static readonly ChunkId EventSetActivators = ChunkId.FromString("TeEventSetActivators"); /********/public static readonly ChunkId EventSetOnEnter = ChunkId.FromString("TeEventSetOnEnter"); @@ -249,5 +250,8 @@ internal static class Prj2Chunks /**/public static readonly ChunkId Palette = ChunkId.FromString("TePalette"); /**/public static readonly ChunkId Favorites = ChunkId.FromString("TeFavorites"); /****/public static readonly ChunkId Favorite = ChunkId.FromString("TeFavorite"); + /**/public static readonly ChunkId CollapsedGlobalEventSetFolders = ChunkId.FromString("TeCollapsedGlbEvtFolders"); + /**/public static readonly ChunkId CollapsedVolumeEventSetFolders = ChunkId.FromString("TeCollapsedVolEvtFolders"); + /****/public static readonly ChunkId CollapsedEventSetFolder = ChunkId.FromString("TeCollapsedEvtFolder"); } } diff --git a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs index 2dce8fb413..e973d9a241 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs @@ -532,6 +532,8 @@ private static bool LoadLevelSettings(ChunkReader chunkIO, ChunkId idOuter, stri eventSetIndex = chunkIO.ReadChunkInt(chunkSize3); else if (id3 == Prj2Chunks.EventSetName) eventSet.Name = chunkIO.ReadChunkString(chunkSize3); + else if (id3 == Prj2Chunks.EventSetFolder) + eventSet.Folder = chunkIO.ReadChunkString(chunkSize3); else if (id3 == Prj2Chunks.EventSetLastUsedEventIndex) eventSet.LastUsedEvent = (EventType)chunkIO.ReadChunkInt(chunkSize3); else if (id3 == Prj2Chunks.EventSetActivators) @@ -731,6 +733,22 @@ private static bool LoadLevelSettings(ChunkReader chunkIO, ChunkId idOuter, stri else return false; }); } + else if (id == Prj2Chunks.CollapsedGlobalEventSetFolders || + id == Prj2Chunks.CollapsedVolumeEventSetFolders) + { + var target = id == Prj2Chunks.CollapsedGlobalEventSetFolders ? settings.CollapsedGlobalEventSetFolders : settings.CollapsedVolumeEventSetFolders; + + target.Clear(); + chunkIO.ReadChunks((id2, chunkSize2) => + { + if (id2 == Prj2Chunks.CollapsedEventSetFolder) + { + target.Add(chunkIO.ReadChunkString(chunkSize2)); + return true; + } + else return false; + }); + } else return false; return true; diff --git a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs index ebec3a9610..04e56c8b72 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs @@ -335,6 +335,8 @@ private static LevelSettingsIds WriteLevelSettings(ChunkWriter chunkIO, LevelSet { chunkIO.WriteChunkInt(Prj2Chunks.EventSetIndex, index); chunkIO.WriteChunkString(Prj2Chunks.EventSetName, set.Name ?? string.Empty); + if (!string.IsNullOrEmpty(set.Folder)) + chunkIO.WriteChunkString(Prj2Chunks.EventSetFolder, set.Folder); chunkIO.WriteChunkInt(Prj2Chunks.EventSetLastUsedEventIndex, (int)set.LastUsedEvent); if (!global) @@ -401,6 +403,19 @@ private static LevelSettingsIds WriteLevelSettings(ChunkWriter chunkIO, LevelSet chunkIO.WriteChunkEnd(); } } + + foreach (bool global in new[] { true, false }) + { + var collapsed = global ? settings.CollapsedGlobalEventSetFolders : settings.CollapsedVolumeEventSetFolders; + + if (collapsed.Count > 0) + using (var chunkCollapsed = chunkIO.WriteChunk(global ? Prj2Chunks.CollapsedGlobalEventSetFolders : Prj2Chunks.CollapsedVolumeEventSetFolders, long.MaxValue)) + { + foreach (var path in collapsed) + chunkIO.WriteChunkString(Prj2Chunks.CollapsedEventSetFolder, path); + chunkIO.WriteChunkEnd(); + } + } chunkIO.WriteChunkEnd(); } diff --git a/TombLib/TombLib/LevelData/LevelSettings.cs b/TombLib/TombLib/LevelData/LevelSettings.cs index 97fa8545fa..8632ce39c2 100644 --- a/TombLib/TombLib/LevelData/LevelSettings.cs +++ b/TombLib/TombLib/LevelData/LevelSettings.cs @@ -7,8 +7,6 @@ using System.Numerics; using System.Reflection; using System.Text.RegularExpressions; -using TombLib.LevelData.IO; -using TombLib.NG; using TombLib.Utils; using TombLib.Wad; using ImportedGeometryUpdateInfo = System.Collections.Generic.KeyValuePair; @@ -204,6 +202,8 @@ public List GlobalSoundMap public List AnimatedTextureSets { get; set; } = new List(); public List GlobalEventSets { get; set; } = new List(); public List VolumeEventSets { get; set; } = new List(); + public HashSet CollapsedGlobalEventSetFolders { get; set; } = new HashSet(); + public HashSet CollapsedVolumeEventSetFolders { get; set; } = new HashSet(); public List Palette { get; set; } = LoadPalette(ResourcesC.ResourcesC.palette); public HashSet Favorites { get; set; } = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -243,7 +243,9 @@ public LevelSettings Clone() result.AnimatedTextureSets = AnimatedTextureSets.ConvertAll(set => set.Clone()); result.ImportedGeometries = ImportedGeometries.ConvertAll(geometry => geometry.Clone()); result.AutoStaticMeshMerges = AutoStaticMeshMerges.ConvertAll(entry => entry.Clone()); - return result; + result.CollapsedGlobalEventSetFolders = new HashSet(CollapsedGlobalEventSetFolders); + result.CollapsedVolumeEventSetFolders = new HashSet(CollapsedVolumeEventSetFolders); + return result; } object ICloneable.Clone() diff --git a/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs b/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs index 08e1e09650..4e0da52f29 100644 --- a/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs +++ b/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using TombLib.LevelData; namespace TombLib.LevelData.VisualScripting { @@ -78,6 +79,19 @@ public class NodeFunction public bool Conditional { get; set; } public string Signature { get; set; } public List Arguments { get; private set; } = new List(); + public List SupportedEvents { get; private set; } = new List(); + public List UnsupportedEvents { get; private set; } = new List(); + + public bool IsUnsupported(EventType eventType) + { + if (SupportedEvents.Count > 0 && !SupportedEvents.Contains(eventType)) + return true; + + if (UnsupportedEvents.Count > 0 && UnsupportedEvents.Contains(eventType)) + return true; + + return false; + } public override string ToString() => Name; public override int GetHashCode() => (Name + Conditional.ToString() + Description + Signature + Arguments.Count.ToString()).GetHashCode(); diff --git a/TombLib/TombLib/Utils/ScriptingUtils.cs b/TombLib/TombLib/Utils/ScriptingUtils.cs index a7bb45da48..c98892e839 100644 --- a/TombLib/TombLib/Utils/ScriptingUtils.cs +++ b/TombLib/TombLib/Utils/ScriptingUtils.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using TombLib.LevelData; using TombLib.LevelData.VisualScripting; namespace TombLib.Utils @@ -59,6 +60,8 @@ public static class ScriptingUtils private const string _nodeTypeId = _metadataPrefix + "condition"; private const string _nodeArgumentId = _metadataPrefix + "arguments"; private const string _nodeDescriptionId = _metadataPrefix + "description"; + private const string _nodeSupportedId = _metadataPrefix + "supported"; + private const string _nodeUnsupportedId = _metadataPrefix + "unsupported"; private const string _nodeLayoutNewLine = "newline"; public static string GameNodeScriptPath = Path.Combine("Scripts", "Engine", "NodeCatalogs"); @@ -156,6 +159,16 @@ private static List GetAllNodeFunctions(string path, List 0) @@ -257,6 +270,16 @@ private static List GetAllNodeFunctions(string path, List n.Section).ToList(); } + private static void ParseEventTypeList(string comment, string tagId, List targetList) + { + var values = TextExtensions.ExtractValues(comment.Substring(tagId.Length)); + foreach (var v in values) + { + if (Enum.TryParse(v.Trim(), true, out EventType eventType) && !targetList.Contains(eventType)) + targetList.Add(eventType); + } + } + public static List GetAllFunctionNames(string path, List list = null, int depth = 0) { var result = list == null ? new List() : list;