diff --git a/DarkUI/DarkUI.WPF/CustomControls/NumericUpDown.cs b/DarkUI/DarkUI.WPF/CustomControls/NumericUpDown.cs index fecda13aaf..0001995f97 100644 --- a/DarkUI/DarkUI.WPF/CustomControls/NumericUpDown.cs +++ b/DarkUI/DarkUI.WPF/CustomControls/NumericUpDown.cs @@ -175,8 +175,10 @@ private void TextBox_PreviewKeyUp(object sender, KeyEventArgs e) private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { - // Prevent character input, but allow the current culture's decimal separator - if (!double.TryParse(e.Text, out _) && e.Text != CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator) + // Allow digits, the current culture's decimal separator, and minus sign. + if (!double.TryParse(e.Text, out _) + && e.Text != CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator + && e.Text != CultureInfo.CurrentCulture.NumberFormat.NegativeSign) e.Handled = true; } diff --git a/TombEditor/Command.cs b/TombEditor/Command.cs index 06ca63f345..a257d21f6c 100644 --- a/TombEditor/Command.cs +++ b/TombEditor/Command.cs @@ -1692,6 +1692,7 @@ static CommandHandler() AddCommand("ShowTexturePanel", "Show texture panel", CommandType.Windows, (CommandArgs args) => args.Editor.ToggleToolWindow(typeof(TexturePanel))); AddCommand("ShowObjectList", "Show object list", CommandType.Windows, (CommandArgs args) => args.Editor.ToggleToolWindow(typeof(ObjectList))); AddCommand("ShowToolPalette", "Show tool palette", CommandType.Windows, (CommandArgs args) => args.Editor.ToggleToolWindow(typeof(ToolPalette))); + AddCommand("ShowItemProperties", "Show item properties", CommandType.Windows, (CommandArgs args) => args.Editor.ToggleToolWindow(typeof(ItemProperties))); AddCommand("ShowStatistics", "Statistics display", CommandType.Windows, delegate (CommandArgs args) { diff --git a/TombEditor/Configuration.cs b/TombEditor/Configuration.cs index 55292a5842..ab87465379 100644 --- a/TombEditor/Configuration.cs +++ b/TombEditor/Configuration.cs @@ -384,7 +384,7 @@ public void EnsureDefaults() { new DockGroupState { - Contents = new List { "TexturePanel" }, + Contents = new List { "TexturePanel", "ItemProperties" }, VisibleContent = "TexturePanel", Order = 0, Size = new Size(286,700) diff --git a/TombEditor/Forms/FormMain.Designer.cs b/TombEditor/Forms/FormMain.Designer.cs index 03c0b78590..f5629bae84 100644 --- a/TombEditor/Forms/FormMain.Designer.cs +++ b/TombEditor/Forms/FormMain.Designer.cs @@ -203,6 +203,7 @@ private void InitializeComponent() paletteToolStripMenuItem = new ToolStripMenuItem(); texturePanelToolStripMenuItem = new ToolStripMenuItem(); objectListToolStripMenuItem = new ToolStripMenuItem(); + luaPropertiesToolStripMenuItem = new ToolStripMenuItem(); statisticsToolStripMenuItem = new ToolStripMenuItem(); dockableToolStripMenuItem = new ToolStripMenuItem(); floatingToolStripMenuItem = new ToolStripMenuItem(); @@ -1867,7 +1868,7 @@ private void InitializeComponent() // windowToolStripMenuItem // windowToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); - windowToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { restoreDefaultLayoutToolStripMenuItem, toolStripMenuSeparator14, sectorOptionsToolStripMenuItem, roomOptionsToolStripMenuItem, itemBrowserToolStripMenuItem, importedGeometryBrowserToolstripMenuItem, contentBrowserToolStripMenuItem, triggerListToolStripMenuItem, lightingToolStripMenuItem, paletteToolStripMenuItem, texturePanelToolStripMenuItem, objectListToolStripMenuItem, statisticsToolStripMenuItem, dockableToolStripMenuItem, floatingToolStripMenuItem }); + windowToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { restoreDefaultLayoutToolStripMenuItem, toolStripMenuSeparator14, sectorOptionsToolStripMenuItem, roomOptionsToolStripMenuItem, itemBrowserToolStripMenuItem, importedGeometryBrowserToolstripMenuItem, contentBrowserToolStripMenuItem, triggerListToolStripMenuItem, lightingToolStripMenuItem, paletteToolStripMenuItem, texturePanelToolStripMenuItem, objectListToolStripMenuItem, luaPropertiesToolStripMenuItem, statisticsToolStripMenuItem, dockableToolStripMenuItem, floatingToolStripMenuItem }); windowToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); windowToolStripMenuItem.Name = "windowToolStripMenuItem"; windowToolStripMenuItem.Size = new System.Drawing.Size(63, 25); @@ -1980,6 +1981,15 @@ private void InitializeComponent() objectListToolStripMenuItem.Tag = "ShowObjectList"; objectListToolStripMenuItem.Text = "ShowObjectList"; // + // luaPropertiesToolStripMenuItem + // + luaPropertiesToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); + luaPropertiesToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + luaPropertiesToolStripMenuItem.Name = "luaPropertiesToolStripMenuItem"; + luaPropertiesToolStripMenuItem.Size = new System.Drawing.Size(246, 22); + luaPropertiesToolStripMenuItem.Tag = "ShowItemProperties"; + luaPropertiesToolStripMenuItem.Text = "ShowItemProperties"; + // // statisticsToolStripMenuItem // statisticsToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); @@ -2445,6 +2455,7 @@ private void InitializeComponent() private ToolStripMenuItem butFindMenu; private ToolStripTextBox tbSearchMenu; private ToolStripMenuItem dockableToolStripMenuItem; + private ToolStripMenuItem luaPropertiesToolStripMenuItem; private ToolStripMenuItem floatingToolStripMenuItem; private ToolStripMenuItem editEventSetsToolStripMenuItem; private ToolStripMenuItem editGlobalEventSetsToolStripMenuItem; diff --git a/TombEditor/Forms/FormMain.cs b/TombEditor/Forms/FormMain.cs index ce06753ef1..bd25e24585 100644 --- a/TombEditor/Forms/FormMain.cs +++ b/TombEditor/Forms/FormMain.cs @@ -37,7 +37,8 @@ public partial class FormMain : DarkForm new Palette(), new TexturePanel(), new ObjectList(), - new ToolPalette() + new ToolPalette(), + new ItemProperties() }; // Floating tool boxes are placed on 3D view at runtime @@ -587,6 +588,7 @@ private void ToolWindow_BuildMenu() lightingToolStripMenuItem.Checked = dockArea.ContainsContent(GetWindow()); paletteToolStripMenuItem.Checked = dockArea.ContainsContent(GetWindow()); texturePanelToolStripMenuItem.Checked = dockArea.ContainsContent(GetWindow()); + luaPropertiesToolStripMenuItem.Checked = dockArea.ContainsContent(GetWindow()); dockableToolStripMenuItem.Checked = dockArea.ContainsContent(GetWindow()); } diff --git a/TombEditor/ToolWindows/ItemProperties.Designer.cs b/TombEditor/ToolWindows/ItemProperties.Designer.cs new file mode 100644 index 0000000000..3c6418bad6 --- /dev/null +++ b/TombEditor/ToolWindows/ItemProperties.Designer.cs @@ -0,0 +1,31 @@ +namespace TombEditor.ToolWindows +{ + partial class ItemProperties + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.SuspendLayout(); + // + // ItemProperties + // + this.DockText = "Item Properties"; + this.Name = "ItemProperties"; + this.SerializationKey = "ItemProperties"; + this.Size = new System.Drawing.Size(300, 400); + this.ResumeLayout(false); + } + + #endregion + } +} diff --git a/TombEditor/ToolWindows/ItemProperties.cs b/TombEditor/ToolWindows/ItemProperties.cs new file mode 100644 index 0000000000..25499f22f4 --- /dev/null +++ b/TombEditor/ToolWindows/ItemProperties.cs @@ -0,0 +1,144 @@ +using DarkUI.Docking; +using System; +using System.Windows.Forms; +using System.Windows.Forms.Integration; +using TombLib.Forms.ViewModels; +using TombLib.Forms.Views; +using TombLib.LevelData; +using TombLib.LuaProperties; + +namespace TombEditor.ToolWindows +{ + public partial class ItemProperties : DarkToolWindow + { + private readonly Editor _editor; + + // WPF hosting + private readonly ElementHost _elementHost; + private readonly LuaPropertyGridControl _wpfControl; + private readonly LuaPropertyGridViewModel _viewModel; + + // Tracked selection + private ObjectInstance _currentObject; + + public ItemProperties() + { + InitializeComponent(); + + _editor = Editor.Instance; + + // Create WPF control + view model. + _viewModel = new LuaPropertyGridViewModel(); + _viewModel.PropertyValueChanged += OnPropertyValueChanged; + + _wpfControl = new LuaPropertyGridControl(); + _wpfControl.ViewModel = _viewModel; + + // ElementHost bridges WPF into the DarkToolWindow. + _elementHost = new ElementHost + { + Dock = DockStyle.Fill, + Child = _wpfControl + }; + this.Controls.Add(_elementHost); + + _editor.EditorEventRaised += EditorEventRaised; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _editor.EditorEventRaised -= EditorEventRaised; + _viewModel.PropertyValueChanged -= OnPropertyValueChanged; + _elementHost?.Dispose(); + } + if (disposing && components != null) + components.Dispose(); + base.Dispose(disposing); + } + + private void EditorEventRaised(IEditorEvent obj) + { + // Respond to selection changes. + if (obj is Editor.SelectedObjectChangedEvent || + obj is Editor.SelectedRoomChangedEvent) + { + UpdatePropertyGrid(); + } + + // Respond to object property changes (e.g. if OCB or slot changed externally). + if (obj is Editor.ObjectChangedEvent objEvent) + { + if (objEvent.Object == _currentObject) + UpdatePropertyGrid(); + } + + // Listen for wad/game version changes to update catalog. + if (obj is Editor.LoadedWadsChangedEvent || + obj is Editor.GameVersionChangedEvent || + obj is Editor.LevelChangedEvent || + obj is Editor.InitEvent) + { + UpdatePropertyGrid(); + } + } + + private void UpdatePropertyGrid() + { + var selected = _editor.SelectedObject; + + // Only show for TombEngine levels. + if (!_editor.Level.IsTombEngine) + { + _viewModel.Clear(); + _viewModel.Title = "Item Properties"; + _viewModel.StatusMessage = "Not supported for this engine target."; + _currentObject = null; + return; + } + + if (selected is MoveableInstance moveable) + { + _currentObject = moveable; + var typeId = moveable.WadObjectId.TypeId; + var definitions = LuaPropertyCatalog.GetDefinitions(LuaPropertyObjectKind.Moveable, typeId); + + // Get wad2 global defaults for this moveable type (if available). + var wadMoveable = _editor.Level.Settings.WadTryGetMoveable(moveable.WadObjectId); + var globalDefaults = wadMoveable?.LuaProperties; + + _viewModel.Title = $"Properties: {moveable.ItemType.ToString()}"; + _viewModel.Load(definitions, moveable.LuaProperties, globalDefaults); + _viewModel.StatusMessage = "No properties defined for this moveable type."; + } + else if (selected is StaticInstance staticObj) + { + _currentObject = staticObj; + var typeId = staticObj.WadObjectId.TypeId; + var definitions = LuaPropertyCatalog.GetDefinitions(LuaPropertyObjectKind.Static, typeId); + + // Get wad2 global defaults for this static type (if available). + var wadStatic = _editor.Level.Settings.WadTryGetStatic(staticObj.WadObjectId); + var globalDefaults = wadStatic?.LuaProperties; + + _viewModel.Title = $"Properties: {staticObj.ItemType.ToString()}"; + _viewModel.Load(definitions, staticObj.LuaProperties, globalDefaults); + _viewModel.StatusMessage = "No properties defined for this static mesh slot."; + } + else + { + _currentObject = null; + _viewModel.Clear(); + _viewModel.Title = "Item Properties"; + _viewModel.StatusMessage = "Select a valid object to edit properties."; + } + } + + private void OnPropertyValueChanged(object sender, EventArgs e) + { + if (_currentObject != null) + _editor.ObjectChange(_currentObject, ObjectChangeType.Change); + } + } +} diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/ArgumentEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/ArgumentEditor.cs index 75ad28187c..8efb7583dc 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/ArgumentEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/ArgumentEditor.cs @@ -7,6 +7,7 @@ using System.Windows.Forms; using TombLib.LevelData; using TombLib.LevelData.VisualScripting; +using TombLib.LuaProperties; using TombLib.Utils; using TombLib.Wad; using TombLib.Wad.Catalog; @@ -366,24 +367,13 @@ private void UnboxValue(string source) { case ArgumentType.Boolean: { - bool result; - if (float.TryParse(source, out float parsedFloat)) - result = parsedFloat == 0.0f ? false : true; - else if (!bool.TryParse(source, out result)) - result = false; - - cbBool.Checked = result; - + cbBool.Checked = LuaValueParser.UnboxBool(source); BoxBoolValue(); break; } case ArgumentType.Numerical: { - float result; - if (bool.TryParse(source, out bool parsedBool)) - result = parsedBool ? 1.0f : 0.0f; - else if (!(float.TryParse(source, out result))) - result = 0.0f; + float result = LuaValueParser.UnboxFloat(source); try { nudNumerical.Value = (decimal)Math.Round(result, nudNumerical.DecimalPlaces); } catch { nudNumerical.Value = (decimal)result < nudNumerical.Minimum ? nudNumerical.Minimum : nudNumerical.Maximum; } @@ -393,16 +383,12 @@ private void UnboxValue(string source) } case ArgumentType.Vector2: { - if (source.StartsWith(LuaSyntax.Vec2TypePrefix + LuaSyntax.BracketOpen) && source.EndsWith(LuaSyntax.BracketClose)) - source = source.Substring(LuaSyntax.Vec2TypePrefix.Length + 1, source.Length - LuaSyntax.Vec2TypePrefix.Length - 2); - - var floats = UnboxVectorValue(source); + source = LuaValueParser.StripTypePrefix(source, LuaSyntax.Vec2TypePrefix); + var floats = LuaValueParser.SplitAndParseFloats(source); for (int i = 0; i < 2; i++) { - var currentFloat = 0.0f; - if (floats.Length > i) - currentFloat = floats[i]; + var currentFloat = floats.Length > i ? floats[i] : 0.0f; try { @@ -427,16 +413,12 @@ private void UnboxValue(string source) } case ArgumentType.Vector3: { - if (source.StartsWith(LuaSyntax.Vec3TypePrefix + LuaSyntax.BracketOpen) && source.EndsWith(LuaSyntax.BracketClose)) - source = source.Substring(LuaSyntax.Vec3TypePrefix.Length + 1, source.Length - LuaSyntax.Vec3TypePrefix.Length - 2); - - var floats = UnboxVectorValue(source); + source = LuaValueParser.StripTypePrefix(source, LuaSyntax.Vec3TypePrefix); + var floats = LuaValueParser.SplitAndParseFloats(source); for (int i = 0; i < 3; i++) { - var currentFloat = 0.0f; - if (floats.Length > i) - currentFloat = floats[i]; + var currentFloat = floats.Length > i ? floats[i] : 0.0f; try { @@ -463,52 +445,38 @@ private void UnboxValue(string source) } case ArgumentType.Color: { - if (source.StartsWith(LuaSyntax.ColorTypePrefix + LuaSyntax.BracketOpen) && source.EndsWith(LuaSyntax.BracketClose)) - source = source.Substring(LuaSyntax.ColorTypePrefix.Length + 1, source.Length - LuaSyntax.ColorTypePrefix.Length - 2); - - var floats = UnboxVectorValue(source); - var bytes = new byte[3] { 0, 0, 0 }; - - for (int i = 0; i < 3; i++) - if (floats.Length > i) - bytes[i] = (byte)MathC.Clamp(floats[i], 0, 255); - - panelColor.BackColor = Color.FromArgb(255, bytes[0], bytes[1], bytes[2]); + var color = LuaValueParser.UnboxColor(source); + panelColor.BackColor = Color.FromArgb(255, color[0], color[1], color[2]); BoxColorValue(); break; } case ArgumentType.Time: { - if (source.StartsWith(LuaSyntax.TimeTypePrefix + LuaSyntax.BracketOpen + LuaSyntax.TableOpen) && source.EndsWith(LuaSyntax.TableClose + LuaSyntax.BracketClose)) - source = source.Substring(LuaSyntax.TimeTypePrefix.Length + 2, source.Length - LuaSyntax.Vec3TypePrefix.Length - 4); - - var floats = UnboxVectorValue(source); + var time = LuaValueParser.UnboxTime(source); - for (int i = 0; i < 5; i++) + for (int i = 0; i < 4; i++) { - var currentFloat = 0.0f; - if (floats.Length > i) - currentFloat = floats[i]; + var currentValue = time[i]; try { switch (i) { - case 0: nudTimeHours.Value = (decimal)currentFloat; break; - case 1: nudTimeMinutes.Value = (decimal)currentFloat; break; - case 2: nudTimeSeconds.Value = (decimal)currentFloat; break; - case 3: nudTimeCents.Value = (decimal)currentFloat; break; + case 0: nudTimeHours.Value = currentValue; break; + case 1: nudTimeMinutes.Value = currentValue; break; + case 2: nudTimeSeconds.Value = currentValue; break; + case 3: nudTimeCents.Value = currentValue; break; } } catch { switch (i) { - case 0: nudTimeHours.Value = (decimal)currentFloat < nudTimeHours.Minimum ? nudTimeHours.Minimum : nudTimeHours.Maximum; break; - case 1: nudTimeMinutes.Value = (decimal)currentFloat < nudTimeMinutes.Minimum ? nudTimeMinutes.Minimum : nudTimeMinutes.Maximum; break; - case 2: nudTimeSeconds.Value = (decimal)currentFloat < nudTimeSeconds.Minimum ? nudTimeSeconds.Minimum : nudTimeSeconds.Maximum; break; - case 3: nudTimeCents.Value = (decimal)currentFloat < nudTimeCents.Minimum ? nudTimeCents.Minimum : nudTimeCents.Maximum; break; + case 0: nudTimeHours.Value = currentValue < nudTimeHours.Minimum ? nudTimeHours.Minimum : nudTimeHours.Maximum; break; + case 1: nudTimeMinutes.Value = currentValue < nudTimeMinutes.Minimum ? nudTimeMinutes.Minimum : nudTimeMinutes.Maximum; break; + case 2: nudTimeSeconds.Value = currentValue < nudTimeSeconds.Minimum ? nudTimeSeconds.Minimum : nudTimeSeconds.Maximum; break; + case 3: nudTimeCents.Value = currentValue < nudTimeCents.Minimum ? nudTimeCents.Minimum : nudTimeCents.Maximum; break; } } } @@ -518,7 +486,7 @@ private void UnboxValue(string source) } case ArgumentType.String: { - tbString.Text = TextExtensions.UnescapeQuotes(TextExtensions.Unquote(source)); + tbString.Text = LuaValueParser.UnboxString(source); BoxStringValue(); break; } @@ -544,78 +512,45 @@ private void UnboxValue(string source) } } - private float[] UnboxVectorValue(string source) - { - return source.Split(new string[] { LuaSyntax.Separator }, StringSplitOptions.None).Select(x => - { - float result; - if (float.TryParse(x.Trim(), out result)) - return result; - else - return 0.0f; - }).ToArray(); - } - private void BoxBoolValue() { - _text = cbBool.Checked.ToString().ToLower(); + _text = LuaValueParser.BoxBool(cbBool.Checked); OnValueChanged(); } private void BoxVector2Value() { - var x = ((float)nudVector2X.Value).ToString(); - var y = ((float)nudVector2Y.Value).ToString(); - _text = LuaSyntax.Vec2TypePrefix + LuaSyntax.BracketOpen + - x + LuaSyntax.Separator + - y + LuaSyntax.BracketClose; + _text = LuaValueParser.BoxVec2((float)nudVector2X.Value, (float)nudVector2Y.Value); OnValueChanged(); } private void BoxVector3Value() { - var x = ((float)nudVector3X.Value).ToString(); - var y = ((float)nudVector3Y.Value).ToString(); - var z = ((float)nudVector3Z.Value).ToString(); - _text = LuaSyntax.Vec3TypePrefix + LuaSyntax.BracketOpen + - x + LuaSyntax.Separator + - y + LuaSyntax.Separator + - z + LuaSyntax.BracketClose; + _text = LuaValueParser.BoxVec3((float)nudVector3X.Value, (float)nudVector3Y.Value, (float)nudVector3Z.Value); OnValueChanged(); } private void BoxTimeValue() { - var h = ((int)nudTimeHours.Value).ToString(); - var m = ((int)nudTimeMinutes.Value).ToString(); - var s = ((int)nudTimeSeconds.Value).ToString(); - var c = ((int)nudTimeCents.Value).ToString(); - _text = LuaSyntax.TimeTypePrefix + LuaSyntax.BracketOpen + LuaSyntax.TableOpen + - h + LuaSyntax.Separator + - m + LuaSyntax.Separator + - s + LuaSyntax.Separator + - c + LuaSyntax.TableClose + LuaSyntax.BracketClose; + _text = LuaValueParser.BoxTime((int)nudTimeHours.Value, (int)nudTimeMinutes.Value, (int)nudTimeSeconds.Value, (int)nudTimeCents.Value); OnValueChanged(); } private void BoxColorValue() { - _text = LuaSyntax.ColorTypePrefix + LuaSyntax.BracketOpen + - panelColor.BackColor.R.ToString() + LuaSyntax.Separator + - panelColor.BackColor.G.ToString() + LuaSyntax.Separator + - panelColor.BackColor.B.ToString() + LuaSyntax.BracketClose; + _text = LuaValueParser.BoxColor(panelColor.BackColor.R, panelColor.BackColor.G, panelColor.BackColor.B); OnValueChanged(); } private void BoxNumericalValue() { - _text = ((float)nudNumerical.Value).ToString(); + _text = LuaValueParser.BoxFloat((float)nudNumerical.Value); OnValueChanged(); } private void BoxStringValue() { - _text = TextExtensions.Quote(TextExtensions.EscapeQuotes(tbString.Text)); + _text = LuaValueParser.BoxString(tbString.Text); OnValueChanged(); } diff --git a/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs b/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs index db7b3d4b2b..15a4426514 100644 --- a/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs +++ b/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs @@ -143,7 +143,21 @@ public static bool CurrentControlSupportsInput(Form form, Keys keyData) return true; } - if ((keyData.HasFlag(Keys.Control | Keys.A) || + if (activeControl is ElementHost && + (keyData.HasFlag(Keys.Control | Keys.A) || + keyData.HasFlag(Keys.Control | Keys.X) || + keyData.HasFlag(Keys.Control | Keys.C) || + keyData.HasFlag(Keys.Control | Keys.V) || + (!keyData.HasFlag(Keys.Control) && !keyData.HasFlag(Keys.Alt)))) + { + var wpfFocused = System.Windows.Input.Keyboard.FocusedElement; + + if (wpfFocused is System.Windows.Controls.TextBox || + wpfFocused is System.Windows.Controls.Primitives.TextBoxBase) + return true; + } + + if ((keyData.HasFlag(Keys.Control | Keys.A) || keyData.HasFlag(Keys.Control | Keys.X) || keyData.HasFlag(Keys.Control | Keys.C) || keyData.HasFlag(Keys.Control | Keys.V) || diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs new file mode 100644 index 0000000000..e2ea1f2a4b --- /dev/null +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs @@ -0,0 +1,184 @@ +// ViewModel for the Lua property grid control. +// Manages a collection of LuaPropertyRowViewModels populated from XML property definitions +// and current values from a LuaPropertyContainer. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using TombLib.LuaProperties; + +namespace TombLib.Forms.ViewModels +{ + /// + /// ViewModel for the . + /// Displays property definitions for a specific object type, populated + /// with current values from a . + /// + public partial class LuaPropertyGridViewModel : ObservableObject + { + /// + /// All property rows, grouped by category. + /// + public ObservableCollection Properties { get; } + = new ObservableCollection(); + + /// + /// Distinct categories present in the current property set. + /// Empty string represents uncategorized properties. + /// + public IEnumerable Categories => + Properties.Select(p => p.Category).Distinct().OrderBy(c => string.IsNullOrEmpty(c) ? "~" : c); + + /// + /// Returns properties grouped by category. + /// Uncategorized properties (empty category) come first. + /// + public IEnumerable> GroupedProperties => + Properties.GroupBy(p => p.Category).OrderBy(g => string.IsNullOrEmpty(g.Key) ? "" : g.Key); + + /// + /// True if no properties are currently loaded. + /// + public bool IsEmpty => Properties.Count == 0; + + /// + /// Fired when any property value changes. The sender is the modified row ViewModel. + /// + public event EventHandler PropertyValueChanged; + + /// + /// Status message displayed when the property grid is empty. + /// + [ObservableProperty] + private string _statusMessage = "No properties defined for this object type."; + + /// + /// The display title for the property grid header. + /// + [ObservableProperty] + private string _title = "Properties"; + + /// + /// The currently bound property container (set during Load). + /// + private LuaPropertyContainer _container; + + /// + /// Loads property definitions for the given object type and populates current values + /// from the provided container. If container is null, defaults are used. + /// + /// Property definitions for this object type (from XML catalog). + /// Existing property container with saved values, or null. + /// Optional global defaults from the wad2 object type. + /// When provided, these values override XML catalog defaults for display and reset purposes. + public void Load(List definitions, LuaPropertyContainer container, LuaPropertyContainer globalDefaults = null) + { + Properties.Clear(); + _container = container; + + if (definitions == null || definitions.Count == 0) + { + OnPropertyChanged(nameof(IsEmpty)); + OnPropertyChanged(nameof(GroupedProperties)); + OnPropertyChanged(nameof(Categories)); + return; + } + + foreach (var definition in definitions) + { + if (!definition.IsValid) + continue; + + // Resolve global default from wad2 (if available) for this property. + string wadDefault = globalDefaults?.GetValue(definition.InternalName); + + // Get the current instance value from container, falling back to + // wad2 global default, then XML catalog default. + string currentValue = container?.GetValue(definition.InternalName) + ?? wadDefault + ?? definition.DefaultValue + ?? LuaValueParser.GetDefaultBoxedValue(definition.Type); + + var row = new LuaPropertyRowViewModel(definition, currentValue, wadDefault); + row.ValueChanged += OnRowValueChanged; + Properties.Add(row); + } + + OnPropertyChanged(nameof(IsEmpty)); + OnPropertyChanged(nameof(GroupedProperties)); + OnPropertyChanged(nameof(Categories)); + } + + /// + /// Writes all current values back to the provided container. + /// Only values that differ from defaults are written. + /// + public void SaveTo(LuaPropertyContainer container) + { + if (container == null) + return; + + container.Clear(); + + foreach (var row in Properties) + { + // Only store values that differ from the catalog default. + if (row.IsModified) + container.SetValue(row.Definition.InternalName, row.BoxedValue); + } + } + + /// + /// Returns a new container with all modified values. + /// + public LuaPropertyContainer ToContainer() + { + var container = new LuaPropertyContainer(); + SaveTo(container); + return container; + } + + /// + /// Resets all properties to their default values. + /// + [RelayCommand] + public void ResetAll() + { + foreach (var row in Properties) + row.ResetToDefault(); + } + + /// + /// Clears the property grid. + /// + public void Clear() + { + foreach (var row in Properties) + row.ValueChanged -= OnRowValueChanged; + + Properties.Clear(); + _container = null; + + OnPropertyChanged(nameof(IsEmpty)); + OnPropertyChanged(nameof(GroupedProperties)); + OnPropertyChanged(nameof(Categories)); + } + + private void OnRowValueChanged(object sender, EventArgs e) + { + // Write back to container immediately when a value changes. + if (sender is LuaPropertyRowViewModel row && _container != null) + { + if (row.IsModified) + _container.SetValue(row.Definition.InternalName, row.BoxedValue); + else + _container.Remove(row.Definition.InternalName); + } + + PropertyValueChanged?.Invoke(sender, e); + } + } +} diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs new file mode 100644 index 0000000000..cca59e0762 --- /dev/null +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; +using TombLib.LuaProperties; + +// ViewModel for a single property row in the Lua property grid. +// Manages the binding between a LuaPropertyDefinition and its current boxed value. + +namespace TombLib.Forms.ViewModels +{ + /// + /// ViewModel for a single row in the Lua property grid. + /// Wraps a with an editable current value. + /// + public partial class LuaPropertyRowViewModel : ObservableObject + { + /// + /// The property definition (read-only metadata). + /// + public LuaPropertyDefinition Definition { get; } + + /// + /// Display name shown in the name column. + /// + public string DisplayName => Definition.DisplayName; + + /// + /// Tooltip text for the property row. + /// + public string ToolTip => string.IsNullOrEmpty(Definition.Description) ? Definition.DisplayName : Definition.Description; + + /// + /// Category grouping label. Empty string means uncategorized. + /// + public string Category => Definition.Category ?? string.Empty; + + /// + /// For Color properties, whether the alpha channel editor is visible. + /// + public bool HasAlpha => Definition.HasAlpha; + + /// + /// The Lua property type. + /// + public LuaPropertyType PropertyType => Definition.Type; + + /// + /// Current boxed Lua value string. + /// Setting this updates all type-specific properties via notification. + /// + public string BoxedValue + { + get => _boxedValue; + set + { + if (_boxedValue == value) + return; + + _boxedValue = value ?? LuaValueParser.GetDefaultBoxedValue(Definition.Type); + + OnPropertyChanged(); + RefreshTypedProperties(); + ValueChanged?.Invoke(this, EventArgs.Empty); + } + } + private string _boxedValue; + + /// + /// Fired when the boxed value changes (used by host to save changes). + /// + public event EventHandler ValueChanged; + + #region Type-specific property accessors (for data binding) + + public bool BoolValue + { + get => LuaValueParser.UnboxBool(_boxedValue); + set => BoxedValue = LuaValueParser.BoxBool(value); + } + + public int IntValue + { + get => LuaValueParser.UnboxInt(_boxedValue); + set => BoxedValue = LuaValueParser.BoxInt(value); + } + + public float FloatValue + { + get => LuaValueParser.UnboxFloat(_boxedValue); + set => BoxedValue = LuaValueParser.BoxFloat(value); + } + + public string StringValue + { + get => LuaValueParser.UnboxString(_boxedValue); + set => BoxedValue = LuaValueParser.BoxString(value); + } + + public float Vec2X + { + get => LuaValueParser.UnboxVec2(_boxedValue)[0]; + set { var v = LuaValueParser.UnboxVec2(_boxedValue); BoxedValue = LuaValueParser.BoxVec2(value, v[1]); } + } + public float Vec2Y + { + get => LuaValueParser.UnboxVec2(_boxedValue)[1]; + set { var v = LuaValueParser.UnboxVec2(_boxedValue); BoxedValue = LuaValueParser.BoxVec2(v[0], value); } + } + + public float Vec3X + { + get => LuaValueParser.UnboxVec3(_boxedValue)[0]; + set { var v = LuaValueParser.UnboxVec3(_boxedValue); BoxedValue = LuaValueParser.BoxVec3(value, v[1], v[2]); } + } + public float Vec3Y + { + get => LuaValueParser.UnboxVec3(_boxedValue)[1]; + set { var v = LuaValueParser.UnboxVec3(_boxedValue); BoxedValue = LuaValueParser.BoxVec3(v[0], value, v[2]); } + } + public float Vec3Z + { + get => LuaValueParser.UnboxVec3(_boxedValue)[2]; + set { var v = LuaValueParser.UnboxVec3(_boxedValue); BoxedValue = LuaValueParser.BoxVec3(v[0], v[1], value); } + } + + public float RotationX + { + get => LuaValueParser.UnboxRotation(_boxedValue)[0]; + set { var v = LuaValueParser.UnboxRotation(_boxedValue); BoxedValue = LuaValueParser.BoxRotation(value, v[1], v[2]); } + } + public float RotationY + { + get => LuaValueParser.UnboxRotation(_boxedValue)[1]; + set { var v = LuaValueParser.UnboxRotation(_boxedValue); BoxedValue = LuaValueParser.BoxRotation(v[0], value, v[2]); } + } + public float RotationZ + { + get => LuaValueParser.UnboxRotation(_boxedValue)[2]; + set { var v = LuaValueParser.UnboxRotation(_boxedValue); BoxedValue = LuaValueParser.BoxRotation(v[0], v[1], value); } + } + + public byte ColorR + { + get => LuaValueParser.UnboxColor(_boxedValue)[0]; + set + { + var c = LuaValueParser.UnboxColor(_boxedValue); + BoxedValue = HasAlpha ? LuaValueParser.BoxColor(value, c[1], c[2], c[3]) : LuaValueParser.BoxColor(value, c[1], c[2]); + } + } + public byte ColorG + { + get => LuaValueParser.UnboxColor(_boxedValue)[1]; + set + { + var c = LuaValueParser.UnboxColor(_boxedValue); + BoxedValue = HasAlpha ? LuaValueParser.BoxColor(c[0], value, c[2], c[3]) : LuaValueParser.BoxColor(c[0], value, c[2]); + } + } + public byte ColorB + { + get => LuaValueParser.UnboxColor(_boxedValue)[2]; + set + { + var c = LuaValueParser.UnboxColor(_boxedValue); + BoxedValue = HasAlpha ? LuaValueParser.BoxColor(c[0], c[1], value, c[3]) : LuaValueParser.BoxColor(c[0], c[1], value); + } + } + public byte ColorA + { + get => LuaValueParser.UnboxColor(_boxedValue)[3]; + set { var c = LuaValueParser.UnboxColor(_boxedValue); BoxedValue = LuaValueParser.BoxColor(c[0], c[1], c[2], value); } + } + + // WPF-bindable color brush for the color picker preview. + public System.Windows.Media.Color WpfColor + { + get + { + var c = LuaValueParser.UnboxColor(_boxedValue); + return System.Windows.Media.Color.FromArgb(c[3], c[0], c[1], c[2]); + } + } + + public int TimeHours + { + get => LuaValueParser.UnboxTime(_boxedValue)[0]; + set { var t = LuaValueParser.UnboxTime(_boxedValue); BoxedValue = LuaValueParser.BoxTime(value, t[1], t[2], t[3]); } + } + public int TimeMinutes + { + get => LuaValueParser.UnboxTime(_boxedValue)[1]; + set { var t = LuaValueParser.UnboxTime(_boxedValue); BoxedValue = LuaValueParser.BoxTime(t[0], value, t[2], t[3]); } + } + public int TimeSeconds + { + get => LuaValueParser.UnboxTime(_boxedValue)[2]; + set { var t = LuaValueParser.UnboxTime(_boxedValue); BoxedValue = LuaValueParser.BoxTime(t[0], t[1], value, t[3]); } + } + public int TimeCentiseconds + { + get => LuaValueParser.UnboxTime(_boxedValue)[3]; + set { var t = LuaValueParser.UnboxTime(_boxedValue); BoxedValue = LuaValueParser.BoxTime(t[0], t[1], t[2], value); } + } + + public IReadOnlyList EnumValues => Definition.EnumValues; + public int EnumIndex + { + get + { + int idx = LuaValueParser.UnboxInt(_boxedValue); + return Math.Max(0, Math.Min(idx, Definition.EnumValues.Count - 1)); + } + set => BoxedValue = LuaValueParser.BoxInt(Math.Max(0, value)); + } + + #endregion + + /// + /// The effective default value for this property. This is the global (wad2) default + /// if one was provided, otherwise the XML catalog default. + /// + public string EffectiveDefault { get; } + + public LuaPropertyRowViewModel(LuaPropertyDefinition definition, string initialBoxedValue = null, string globalDefault = null) + { + Definition = definition ?? throw new ArgumentNullException(nameof(definition)); + EffectiveDefault = globalDefault ?? definition.DefaultValue ?? LuaValueParser.GetDefaultBoxedValue(definition.Type); + _boxedValue = initialBoxedValue ?? EffectiveDefault; + } + + /// + /// Resets the value to the effective default (wad2 global value, or XML catalog default). + /// + public void ResetToDefault() + { + BoxedValue = EffectiveDefault; + } + + /// + /// Returns true if the current value differs from the effective default. + /// + public bool IsModified => !string.Equals(_boxedValue, EffectiveDefault, StringComparison.Ordinal); + + /// + /// Notifies all typed property bindings that the underlying value changed. + /// + private void RefreshTypedProperties() + { + switch (Definition.Type) + { + case LuaPropertyType.Bool: + OnPropertyChanged(nameof(BoolValue)); + break; + case LuaPropertyType.Int: + OnPropertyChanged(nameof(IntValue)); + break; + case LuaPropertyType.Float: + OnPropertyChanged(nameof(FloatValue)); + break; + case LuaPropertyType.String: + OnPropertyChanged(nameof(StringValue)); + break; + case LuaPropertyType.Vec2: + OnPropertyChanged(nameof(Vec2X)); + OnPropertyChanged(nameof(Vec2Y)); + break; + case LuaPropertyType.Vec3: + OnPropertyChanged(nameof(Vec3X)); + OnPropertyChanged(nameof(Vec3Y)); + OnPropertyChanged(nameof(Vec3Z)); + break; + case LuaPropertyType.Rotation: + OnPropertyChanged(nameof(RotationX)); + OnPropertyChanged(nameof(RotationY)); + OnPropertyChanged(nameof(RotationZ)); + break; + case LuaPropertyType.Color: + OnPropertyChanged(nameof(ColorR)); + OnPropertyChanged(nameof(ColorG)); + OnPropertyChanged(nameof(ColorB)); + OnPropertyChanged(nameof(ColorA)); + OnPropertyChanged(nameof(WpfColor)); + break; + case LuaPropertyType.Time: + OnPropertyChanged(nameof(TimeHours)); + OnPropertyChanged(nameof(TimeMinutes)); + OnPropertyChanged(nameof(TimeSeconds)); + OnPropertyChanged(nameof(TimeCentiseconds)); + break; + case LuaPropertyType.Enum: + OnPropertyChanged(nameof(EnumIndex)); + break; + } + } + } +} diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs b/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs new file mode 100644 index 0000000000..a72f3aff4c --- /dev/null +++ b/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs @@ -0,0 +1,50 @@ +using System.Windows; +using System.Windows.Controls; +using TombLib.LuaProperties; + +// DataTemplateSelector that picks the correct editor template +// based on the Lua property type (bool, int, float, string, Vec2, Vec3, etc.). +// Used by LuaPropertyGridControl to dynamically render the value column. + +namespace TombLib.Forms.Views +{ + /// + /// Selects the correct DataTemplate for a property row's value editor + /// based on the property's . + /// + public class LuaPropertyEditorTemplateSelector : DataTemplateSelector + { + public DataTemplate BoolTemplate { get; set; } + public DataTemplate IntTemplate { get; set; } + public DataTemplate FloatTemplate { get; set; } + public DataTemplate StringTemplate { get; set; } + public DataTemplate Vec2Template { get; set; } + public DataTemplate Vec3Template { get; set; } + public DataTemplate RotationTemplate { get; set; } + public DataTemplate ColorTemplate { get; set; } + public DataTemplate TimeTemplate { get; set; } + public DataTemplate EnumTemplate { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + if (item is ViewModels.LuaPropertyRowViewModel row) + { + switch (row.PropertyType) + { + case LuaPropertyType.Bool: return BoolTemplate; + case LuaPropertyType.Int: return IntTemplate; + case LuaPropertyType.Float: return FloatTemplate; + case LuaPropertyType.String: return StringTemplate; + case LuaPropertyType.Vec2: return Vec2Template; + case LuaPropertyType.Vec3: return Vec3Template; + case LuaPropertyType.Rotation: return RotationTemplate; + case LuaPropertyType.Color: return ColorTemplate; + case LuaPropertyType.Time: return TimeTemplate; + case LuaPropertyType.Enum: return EnumTemplate; + } + } + + return base.SelectTemplate(item, container); + } + } +} diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml new file mode 100644 index 0000000000..5f2d863377 --- /dev/null +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml @@ -0,0 +1,341 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs new file mode 100644 index 0000000000..7087c23182 --- /dev/null +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs @@ -0,0 +1,109 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using DarkUI.WPF.CustomControls; +using TombLib.Controls; +using TombLib.Forms.ViewModels; + +// Code-behind for LuaPropertyGridControl. +// Handles color picker interaction (opens RealtimeColorDialog via WinForms interop) +// and provides value converters used in the XAML. + +namespace TombLib.Forms.Views +{ + public partial class LuaPropertyGridControl : UserControl + { + public LuaPropertyGridControl() + { + // Add custom converters to resources before InitializeComponent. + Resources.Add("BoolToVisConverter", new BooleanToVisibilityConverter()); + Resources.Add("StringEmptyToVisConverter", new StringEmptyToCollapsedConverter()); + + InitializeComponent(); + } + + public LuaPropertyGridViewModel ViewModel + { + get => DataContext as LuaPropertyGridViewModel; + set => DataContext = value; + } + + private void ColorPicker_Click(object sender, RoutedEventArgs e) + { + if (sender is not ColorPickerButton button || button.DataContext is not LuaPropertyRowViewModel row) + return; + + // Get current color from ViewModel. + var currentColor = System.Drawing.Color.FromArgb(255, row.ColorR, row.ColorG, row.ColorB); + + // Open the existing RealtimeColorDialog (WinForms control). + var mousePos = System.Windows.Forms.Control.MousePosition; + using (var colorDialog = new RealtimeColorDialog(mousePos.X, mousePos.Y, c => + { + // Live preview callback: update color values in real time. + row.ColorR = c.R; + row.ColorG = c.G; + row.ColorB = c.B; + })) + { + colorDialog.Color = currentColor; + colorDialog.FullOpen = true; + + if (colorDialog.ShowDialog() != System.Windows.Forms.DialogResult.OK) + { + // Restore original color on cancel. + row.ColorR = currentColor.R; + row.ColorG = currentColor.G; + row.ColorB = currentColor.B; + return; + } + + // Apply selected color. + if (currentColor != colorDialog.Color) + { + row.ColorR = colorDialog.Color.R; + row.ColorG = colorDialog.Color.G; + row.ColorB = colorDialog.Color.B; + } + } + } + + private void ComboBox_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (sender is ComboBox comboBox && comboBox.Items.Count > 0) + { + int newIndex = comboBox.SelectedIndex + (e.Delta > 0 ? -1 : 1); + newIndex = Math.Max(0, Math.Min(newIndex, comboBox.Items.Count - 1)); + comboBox.SelectedIndex = newIndex; + e.Handled = true; + } + } + + private void PropertyName_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + // Reset property to default when double-clicked on property label. + if (e.ClickCount == 2 && sender is FrameworkElement element && element.DataContext is LuaPropertyRowViewModel row) + { + row.ResetToDefault(); + e.Handled = true; + } + } + } + + internal class StringEmptyToCollapsedConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var str = value as string; + + // Collapses element when string is null or empty. Used for category headers — hides header when category is empty. + return string.IsNullOrEmpty(str) ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); + } +} diff --git a/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml b/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml new file mode 100644 index 0000000000..70c2977964 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TombLib/TombLib/Catalogs/TEN Property Catalogs/README.md b/TombLib/TombLib/Catalogs/TEN Property Catalogs/README.md new file mode 100644 index 0000000000..4952e4faca --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Property Catalogs/README.md @@ -0,0 +1,265 @@ +# TEN Property Catalog Format + +Property catalog files define custom Lua properties for TombEngine moveable and static object types. They are loaded automatically by Tomb Editor and WadTool from this folder at startup. Multiple XML files are merged; if the same property `internalName` is defined for the same object type in more than one file, the last file loaded (alphabetical order) takes priority. + +--- + +## File Structure + +```xml + + + + + + + + + + + + +``` + +The root element must be ``. It may contain any number of `` and `` children in any order. + +--- + +## Object Elements — `` and `` + +| Attribute | Required | Description | +|-----------|----------|-------------| +| `id` | Yes | Numeric slot ID(s) this block targets. Supports all formats below. | + +### `id` Formats + +| Format | Example | Description | +|---------|-------------------|------------------------------------| +| Single | `id="73"` | One slot | +| List | `id="73,74,75"` | Explicit list of slots | +| Range | `id="73-80"` | Inclusive range | +| Mixed | `id="0-5,73,100"` | Any combination of the above | + +Each `` or `` block contains one or more `` child elements. The same block may be repeated for the same ID in different files — properties are merged by `internalName`. + +--- + +## Property Element — `` + +```xml + +``` + +### Attributes + +| Attribute | Required | Default | Description | +|----------------|-------------------|----------------------|-------------| +| `internalName` | **Yes** | — | Lua API property name used in `SetProperty` / `GetProperty` calls. Case-insensitive in the editor; exact case is passed to the Lua API. | +| `displayName` | **Yes** | — | Human-readable label shown in the property grid. | +| `type` | **Yes** | — | Value type. See [Supported Types](#supported-types). | +| `defaultValue` | No | Type-specific zero | Initial value for new objects. Accepts both `defaultValue` and the shorter alias `default`. | +| `category` | No | *(none)* | Groups properties under a collapsible header in the grid. Properties without a category appear at the top level. | +| `description` | No | *(none)* | Tooltip text shown when hovering over the property name or value. | +| `hasAlpha` | No (`Color` only) | `false` | When `true`, an extra alpha (opacity) numeric field is shown next to the color picker. Ignored for all other types. | +| `entries` | No (`Enum` only) | — | Comma-separated list of entry names, e.g. `"Normal, Aggressive, Calm"`. Alternative to child `` nodes. | + +> **Tip:** `displayName` falls back to `internalName` if omitted, but it is strongly recommended to always provide a friendly label. + +--- + +## Supported Types + +### `Bool` + +A true/false toggle. Rendered as a checkbox. + +| Attribute | Format | Example | +|---------------|-------------------|-----------| +| `defaultValue`| `true` or `false` | `"false"` | + +Compiles to Lua `true` / `false`. + +--- + +### `Int` + +A whole number. Rendered as a numeric spinner. + +| Attribute | Format | Example | +|---------------|-------------------|---------| +| `defaultValue`| Integer literal | `"100"` | + +Compiles to a Lua integer literal. + +--- + +### `Float` + +A decimal number. Rendered as a numeric spinner with two decimal places. + +| Attribute | Format | Example | +|---------------|--------------------|-----------| +| `defaultValue`| Floating-point literal (`.` separator) | `"3.14"` | + +Compiles to a Lua number literal. + +--- + +### `String` + +Free text. Rendered as a text box. + +| Attribute | Format | Example | +|---------------|--------------|--------------| +| `defaultValue`| Any text | `"Lara"` | + +Compiles to a quoted Lua string. Inner quotes and backslashes are escaped automatically. + +--- + +### `Vec2` + +A 2D vector (X, Y). Rendered as two numeric fields. + +| Attribute | Format | Example | +|---------------|-------------------------------------|------------| +| `defaultValue`| Two comma-separated floats | `"20, 28"` | + +Compiles to `TEN.Vec2(x, y)`. + +--- + +### `Vec3` + +A 3D vector (X, Y, Z). Rendered as three numeric fields. + +| Attribute | Format | Example | +|---------------|-------------------------------------|---------------| +| `defaultValue`| Three comma-separated floats | `"0, 100, 0"` | + +Compiles to `TEN.Vec3(x, y, z)`. + +--- + +### `Rotation` + +Three Euler angles in degrees (X, Y, Z), each clamped to 0–360. Rendered as three numeric fields. + +| Attribute | Format | Example | +|---------------|-------------------------------------|--------------| +| `defaultValue`| Three comma-separated floats | `"0, 90, 0"` | + +Compiles to `TEN.Rotation(x, y, z)`. + +--- + +### `Color` + +An RGB or RGBA color. Rendered as a color picker button. The alpha field is hidden unless `hasAlpha="true"`. + +| Attribute | Format | Example | +|---------------|-------------------------------------------------|---------------------------| +| `defaultValue`| Three or four comma-separated 0–255 integers | `"255, 128, 0"` or `"255, 0, 0, 128"` | +| `hasAlpha` | `true` / `false` | `"true"` | + +Compiles to `TEN.Color(r, g, b)` or `TEN.Color(r, g, b, a)`. + +--- + +### `Time` + +Hours, minutes, seconds and centiseconds. Rendered as four labeled text fields. + +| Attribute | Format | Example | +|---------------|-------------------------------------------------|-----------------| +| `defaultValue`| Four comma-separated integers: `h, m, s, cs` | `"0, 1, 30, 0"` | + +Compiles to `TEN.Time({h, m, s, cs})`. + +--- + +### `Enum` + +A named selection backed by a 0-based integer index. Rendered as a dropdown (ComboBox). + +The entry list may be provided in two ways: + +**Option A — inline `entries` attribute (compact):** + +```xml + +``` + +**Option B — child `` nodes (verbose, suitable for many entries):** + +```xml + + + + + + +``` + +Both forms are equivalent. If both are present, the `entries` attribute takes precedence. + +| Attribute | Format | Example | +|---------------|-----------------------------------------------------|------------| +| `entries` | Comma-separated entry names | `"A, B, C"` | +| `defaultValue`| Entry name **or** 0-based integer index | `"Normal"` or `"0"` | + +Compiles to a Lua integer: the first entry is `0`, the second is `1`, and so on. + +--- + +## Three-Level Property System + +Properties operate at three levels that the editor resolves at runtime: + +| Level | Scope | Where edited | +|-------|-------|--------------| +| **Level 1 — Global** | A given object type refers to a default property value in the catalog | Any text editor (stored in .xml) | +| **Level 2 — Wad** | All instances of an object type share one value | WadTool (stored in `.wad2`) | +| **Level 3 — Instance** | A single placed object overrides the global value | Tomb Editor (stored in `.prj2`) | + +The compiled Lua script blob is embedded into a level file, executed on level startup, and contains calls for +two layers rather than three, because Level 1 and Level 2 collapse into one: + +```lua +-- Level 1+2: type-wide defaults applied in global-to-wad order (if wad property is available) +TEN.Objects.SetMoveableProperty(TEN.Objects.ObjID.BADDY1, "behavior", 1) + +-- Level 3: per-instance override (only when explicitly changed) +TEN.Objects.GetMoveableByName("baddy_boss"):SetProperty("behavior", 2) +``` + +--- + +## File Naming and Organisation + +- Any `.xml` file anywhere inside this folder (including sub-folders) is loaded. +- Files are processed in alphabetical path order. Later files override earlier ones for matching `internalName` + object keys. +- Organise files however you prefer — by object, category, or game chapter. + +--- + +## Example + +See [Example.xml](Example.xml) for a runnable reference covering all types. diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs index 3da0cda49d..128a4059e7 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Threading; using TombLib.LevelData.SectorEnums; +using TombLib.LuaProperties; using TombLib.Utils; using TombLib.Wad; using TombLib.Wad.Catalog; @@ -64,6 +65,7 @@ public int Compare(TombEngineBucket x, TombEngineBucket y) private readonly List _items = new List(); private List _aiItems = new List(); + private KeyValuePair _luaPropertyScriptBlob = new KeyValuePair(); private TombEngineTexInfoManager _textureInfoManager; @@ -142,6 +144,7 @@ public override CompilerStatistics CompileLevel(CancellationToken cancelToken) _progressReporter.ReportInfo("\nWriting level file...\n"); + BuildLuaPropertyScript(); WriteLevelTombEngine(); cancelToken.ThrowIfCancellationRequested(); @@ -543,6 +546,109 @@ private void CopyNodeScripts() }); } + private void BuildLuaPropertyScript() + { + var gameVersion = _level.Settings.GameVersion; + + var globalMovProps = new Dictionary(); + var globalStaticProps = new Dictionary(); + + // Level 1: Global properties. + + foreach (var wadRef in _level.Settings.Wads) + { + if (wadRef.Wad == null) + continue; + + foreach (var mov in wadRef.Wad.Moveables) + { + string slotName = TrCatalog.GetMoveableName(gameVersion, mov.Key.TypeId); + if (string.IsNullOrEmpty(slotName)) + continue; + + // 1) Prefer properties from wad2. + if (mov.Value.LuaProperties != null && mov.Value.LuaProperties.HasProperties) + { + globalMovProps[slotName] = mov.Value.LuaProperties; + continue; + } + + // 2) Fallback to catalog defaults (only if not already defined). + if (globalMovProps.ContainsKey(slotName)) + continue; + + var definitions = LuaPropertyCatalog.GetDefinitions(LuaPropertyObjectKind.Moveable, mov.Key.TypeId); + if (definitions.Count == 0) + continue; + + var container = new LuaPropertyContainer(); + foreach (var def in definitions) + container.SetValue(def.InternalName, def.DefaultValue); + + globalMovProps[slotName] = container; + } + + foreach (var stat in wadRef.Wad.Statics) + { + uint typeId = stat.Key.TypeId; + + // 1) Prefer properties from wad2. + if (stat.Value.LuaProperties != null && stat.Value.LuaProperties.HasProperties) + { + globalStaticProps[typeId] = stat.Value.LuaProperties; + continue; + } + + // 2) Fallback to catalog defaults (only if not already defined). + if (globalStaticProps.ContainsKey(typeId)) + continue; + + var definitions = LuaPropertyCatalog.GetDefinitions(LuaPropertyObjectKind.Static, typeId); + + if (definitions.Count == 0) + continue; + + var container = new LuaPropertyContainer(); + foreach (var def in definitions) + container.SetValue(def.InternalName, def.DefaultValue); + + globalStaticProps[typeId] = container; + } + } + + // Level 2: Instance properties. + + var instanceMovProps = new Dictionary(); + var instanceStaticProps = new Dictionary(); + + foreach (var room in _level.ExistingRooms) + { + foreach (var obj in room.Objects) + { + if (obj is MoveableInstance mov && _level.Settings.WadTryGetMoveable(mov.WadObjectId) != null && + mov.LuaProperties?.HasProperties == true && !string.IsNullOrEmpty(mov.LuaName)) + { + instanceMovProps[mov.LuaName] = mov.LuaProperties; + } + else if (obj is StaticInstance stat && _level.Settings.WadTryGetStatic(stat.WadObjectId) != null && + stat.LuaProperties?.HasProperties == true && !string.IsNullOrEmpty(stat.LuaName)) + { + instanceStaticProps[stat.LuaName] = stat.LuaProperties; + } + } + } + + bool hasAnyProperties = globalMovProps.Count > 0 || globalStaticProps.Count > 0 || instanceMovProps.Count > 0 || instanceStaticProps.Count > 0; + + if (!hasAnyProperties) + { + _luaPropertyScriptBlob = new KeyValuePair(0, string.Empty); + return; + } + + _luaPropertyScriptBlob = LuaPropertyScriptBuilder.BuildFullPropertyScript(globalMovProps, globalStaticProps, instanceMovProps, instanceStaticProps); + } + public bool CheckTombEngineVersion() { var path = _level.Settings.MakeAbsolute(_level.Settings.GameExecutableFilePath); diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs index 3b73f2eda2..e2d040e032 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs @@ -113,6 +113,10 @@ private void WriteLevelTombEngine() set.Write(writer, _level.Settings.VolumeEventSets); } + // Write Lua property script blob + writer.Write(_luaPropertyScriptBlob.Key); + writer.Write(_luaPropertyScriptBlob.Value ?? string.Empty); + dynamicDataBuffer = dynamicDataStream.ToArray(); } diff --git a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs index 591b81e0b5..71efbb8ed7 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs @@ -176,12 +176,14 @@ internal static class Prj2Chunks /**********/public static readonly ChunkId ObjectMovable4 = ChunkId.FromString("TeMov4"); /**********/public static readonly ChunkId ObjectMovableTombEngine = ChunkId.FromString("TeMovTen"); /**********/public static readonly ChunkId ObjectMovableTombEngine2 = ChunkId.FromString("TeMovTen2"); + /**********/public static readonly ChunkId ObjectMovableTombEngine3 = ChunkId.FromString("TeMovTn3"); /**********/public static readonly ChunkId ObjectItemLuaId = ChunkId.FromString("TeItLuaId"); // DEPRECATED /**********/public static readonly ChunkId ObjectStatic = ChunkId.FromString("TeSta"); /**********/public static readonly ChunkId ObjectStatic2 = ChunkId.FromString("TeSta2"); /**********/public static readonly ChunkId ObjectStatic3 = ChunkId.FromString("TeSta3"); /**********/public static readonly ChunkId ObjectStaticTombEngine = ChunkId.FromString("TeStaTen"); /**********/public static readonly ChunkId ObjectStaticTombEngine2 = ChunkId.FromString("TeStaTen2"); + /**********/public static readonly ChunkId ObjectStaticTombEngine3 = ChunkId.FromString("TeStaTn3"); /**********/public static readonly ChunkId ObjectCamera = ChunkId.FromString("TeCam"); /**********/public static readonly ChunkId ObjectCamera2 = ChunkId.FromString("TeCam2"); /**********/public static readonly ChunkId ObjectCamera3 = ChunkId.FromString("TeCam3"); diff --git a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs index d861e9374b..924d784fb4 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Threading.Tasks; using TombLib.IO; +using TombLib.LuaProperties; using TombLib.Utils; using TombLib.Wad; using TombLib.LevelData.VisualScripting; @@ -1261,7 +1262,8 @@ private static bool LoadObjects(ChunkReader chunkIO, ChunkId idOuter, LevelSetti addObject(instance); newObjects.TryAdd(objectID, instance); } - else if (id3 == Prj2Chunks.ObjectMovableTombEngine2) + else if (id3 == Prj2Chunks.ObjectMovableTombEngine2 || + id3 == Prj2Chunks.ObjectMovableTombEngine3) { var instance = new MoveableInstance(); instance.Position = chunkIO.Raw.ReadVector3(); @@ -1276,6 +1278,10 @@ private static bool LoadObjects(ChunkReader chunkIO, ChunkId idOuter, LevelSetti instance.CodeBits = chunkIO.Raw.ReadByte(); instance.Color = chunkIO.Raw.ReadVector3(); instance.LuaName = chunkIO.Raw.ReadStringUTF8(); + + if (id3 == Prj2Chunks.ObjectMovableTombEngine3) + ReadLuaProperties(chunkIO, instance.LuaProperties); + addObject(instance); newObjects.TryAdd(objectID, instance); } @@ -1318,7 +1324,8 @@ private static bool LoadObjects(ChunkReader chunkIO, ChunkId idOuter, LevelSetti instance.LuaName = chunkIO.Raw.ReadStringUTF8(); addObject(instance); } - else if (id3 == Prj2Chunks.ObjectStaticTombEngine2) + else if (id3 == Prj2Chunks.ObjectStaticTombEngine2 || + id3 == Prj2Chunks.ObjectStaticTombEngine3) { var instance = new StaticInstance(); newObjects.TryAdd(objectID, instance); @@ -1332,6 +1339,10 @@ private static bool LoadObjects(ChunkReader chunkIO, ChunkId idOuter, LevelSetti instance.Color = chunkIO.Raw.ReadVector3(); instance.Ocb = chunkIO.Raw.ReadInt16(); instance.LuaName = chunkIO.Raw.ReadStringUTF8(); + + if (id3 == Prj2Chunks.ObjectStaticTombEngine3) + ReadLuaProperties(chunkIO, instance.LuaProperties); + addObject(instance); } else if (id3 == Prj2Chunks.ObjectCamera) @@ -2029,6 +2040,20 @@ private static TriggerNode LoadNode(ChunkReader chunkIO, TriggerNode previous = return (uint)read; } + private static void ReadLuaProperties(ChunkReader chunkIO, LuaPropertyContainer container) + { + int count = chunkIO.Raw.ReadInt32(); + + for (int i = 0; i < count; i++) + { + string name = chunkIO.Raw.ReadStringUTF8(); + string value = chunkIO.Raw.ReadStringUTF8(); + + if (!string.IsNullOrEmpty(name) && value != null) + container.SetValue(name, value); + } + } + private static void TryAdd(this Dictionary this_, K key, T value) { if (!this_.ContainsKey(key)) diff --git a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs index 4be3054842..09714ed505 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs @@ -9,6 +9,7 @@ using TombLib.LevelData.SectorEnums; using TombLib.LevelData.SectorStructs; using TombLib.LevelData.VisualScripting; +using TombLib.LuaProperties; using TombLib.Utils; namespace TombLib.LevelData.IO @@ -563,7 +564,7 @@ private static void WriteObjects(ChunkWriter chunkIO, IEnumerable p.Key).ToList(); + chunkIO.Raw.Write(props.Count); + + foreach (var prop in props) + { + chunkIO.Raw.WriteStringUTF8(prop.Key); + chunkIO.Raw.WriteStringUTF8(prop.Value); + } + } } } diff --git a/TombLib/TombLib/LevelData/Instances/MoveableInstance.cs b/TombLib/TombLib/LevelData/Instances/MoveableInstance.cs index 77f933a6d9..99e230e354 100644 --- a/TombLib/TombLib/LevelData/Instances/MoveableInstance.cs +++ b/TombLib/TombLib/LevelData/Instances/MoveableInstance.cs @@ -1,4 +1,5 @@ using System.Numerics; +using TombLib.LuaProperties; using TombLib.Wad; namespace TombLib.LevelData @@ -30,5 +31,7 @@ public float Roll set { _roll = value; } } private float _roll = 0.0f; + + public LuaPropertyContainer LuaProperties { get; set; } = new LuaPropertyContainer(); } } diff --git a/TombLib/TombLib/LevelData/Instances/StaticInstance.cs b/TombLib/TombLib/LevelData/Instances/StaticInstance.cs index 3cb0f1bb23..49c71eee20 100644 --- a/TombLib/TombLib/LevelData/Instances/StaticInstance.cs +++ b/TombLib/TombLib/LevelData/Instances/StaticInstance.cs @@ -1,4 +1,5 @@ using System.Numerics; +using TombLib.LuaProperties; using TombLib.Wad; namespace TombLib.LevelData @@ -53,5 +54,7 @@ public float Scale set { _scale = value; } } private float _scale = 1.0f; + + public LuaPropertyContainer LuaProperties { get; set; } = new LuaPropertyContainer(); } } diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs b/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs new file mode 100644 index 0000000000..0298721d42 --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs @@ -0,0 +1,407 @@ +using NLog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using TombLib.Utils; + +// XML catalog loader for Lua property definitions. +// Reads XML files from "Catalogs/TEN Property Catalogs" folder, parses property definitions +// per object type (moveable/static by ID), and validates all values against their declared types. +// Supports multi-slot ID syntax: "0", "0,1,2", "0-5", "0-5, 73, 100-105". + +namespace TombLib.LuaProperties +{ + /// + /// Specifies whether a property catalog entry targets a moveable or a static object type. + /// + public enum LuaPropertyObjectKind + { + Moveable, + Static + } + + /// + /// A key identifying an object type for property definitions. + /// Combines the object's kind (moveable/static) with its numeric slot ID. + /// + public struct LuaPropertyObjectKey : IEquatable + { + public LuaPropertyObjectKind Kind; + public uint TypeId; + + public LuaPropertyObjectKey(LuaPropertyObjectKind kind, uint typeId) + { + Kind = kind; + TypeId = typeId; + } + + public bool Equals(LuaPropertyObjectKey other) => Kind == other.Kind && TypeId == other.TypeId; + public override bool Equals(object obj) => obj is LuaPropertyObjectKey other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Kind, TypeId); + public static bool operator ==(LuaPropertyObjectKey a, LuaPropertyObjectKey b) => a.Equals(b); + public static bool operator !=(LuaPropertyObjectKey a, LuaPropertyObjectKey b) => !a.Equals(b); + + public override string ToString() => $"{Kind}:{TypeId}"; + } + + /// + /// Loads and caches Lua property definitions from XML catalog files. + /// Multiple XML files within the catalog folder are merged; + /// if the same property for the same object type is defined in multiple files, + /// the latest one loaded takes priority. + /// + public static class LuaPropertyCatalog + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + /// + /// Path to the property catalog folder relative to program directory. + /// + public static string PropertyCatalogPath => Path.Combine(DefaultPaths.CatalogsDirectory, "TEN Property Catalogs"); + + /// + /// Cached property definitions keyed by object type. + /// + private static Dictionary> _catalog; + + /// + /// Gets all property definitions, loading from disk on first access. + /// + public static Dictionary> Catalog + { + get + { + if (_catalog == null) + _catalog = LoadCatalog(PropertyCatalogPath); + + return _catalog; + } + } + + /// + /// Forces a reload of the catalog from disk. + /// + public static void ReloadCatalog() + { + _catalog = LoadCatalog(PropertyCatalogPath); + } + + /// + /// Gets property definitions for a specific object type. + /// Returns an empty list if no definitions exist. + /// + public static List GetDefinitions(LuaPropertyObjectKind kind, uint typeId) + { + var key = new LuaPropertyObjectKey(kind, typeId); + + if (Catalog.TryGetValue(key, out var definitions)) + return definitions; + + return new List(); + } + + /// + /// Loads all XML files from the specified catalog path + /// and merges them into a single dictionary. + /// + public static Dictionary> LoadCatalog(string path) + { + var result = new Dictionary>(); + + if (!Directory.Exists(path)) + { + logger.Info("Property catalog directory not found: {0}", path); + return result; + } + + var xmlFiles = Directory.GetFiles(path, "*.xml", SearchOption.AllDirectories).OrderBy(f => f).ToList(); + + if (xmlFiles.Count == 0) + { + logger.Info("No XML property catalog files found in: {0}", path); + return result; + } + + foreach (var file in xmlFiles) + { + try + { + LoadCatalogFile(file, result); + } + catch (Exception ex) + { + logger.Error(ex, "Failed to load property catalog file: {0}", file); + } + } + + logger.Info("Loaded property catalogs: {0} object types with properties", result.Count); + return result; + } + + /// + /// Loads a single XML catalog file and merges definitions into the result dictionary. + /// Later-loaded properties with the same InternalName for the same object key overwrite earlier ones. + /// + private static void LoadCatalogFile(string filePath, Dictionary> result) + { + var doc = new XmlDocument(); + doc.Load(filePath); + + var root = doc.DocumentElement; + if (root == null) + { + logger.Warn("Empty XML document: {0}", filePath); + return; + } + + // Process entries. + foreach (XmlNode moveableNode in root.SelectNodes("//moveable")) + ParseObjectNode(moveableNode, LuaPropertyObjectKind.Moveable, filePath, result); + + // Process entries. + foreach (XmlNode staticNode in root.SelectNodes("//static")) + ParseObjectNode(staticNode, LuaPropertyObjectKind.Static, filePath, result); + } + + /// + /// Parses a single <moveable> or <static> XML node and extracts its + /// child <property> definitions. + /// Supports multi-slot id formats: "0", "0,1,2", "0-5", "0-5, 73, 100-105". + /// + private static void ParseObjectNode(XmlNode objectNode, LuaPropertyObjectKind kind, string filePath, Dictionary> result) + { + var idAttr = objectNode.Attributes?["id"]; + if (idAttr == null || string.IsNullOrWhiteSpace(idAttr.Value)) + { + logger.Warn("Property catalog entry missing 'id' attribute in {0}", filePath); + return; + } + + var typeIds = ParseIdList(idAttr.Value, filePath); + if (typeIds.Count == 0) + { + logger.Warn("Property catalog entry has no valid IDs in '{0}' in {1}", idAttr.Value, filePath); + return; + } + + // Parse properties once, then assign to all target IDs. + var definitions = new List(); + foreach (XmlNode propNode in objectNode.SelectNodes("property")) + { + var definition = ParsePropertyNode(propNode, filePath); + if (definition != null && definition.IsValid) + definitions.Add(definition); + } + + foreach (uint typeId in typeIds) + { + var key = new LuaPropertyObjectKey(kind, typeId); + + if (!result.ContainsKey(key)) + result[key] = new List(); + + foreach (var definition in definitions) + { + // If same internal name exists, replace it (latest file wins). + var existingIndex = result[key].FindIndex(p => + string.Equals(p.InternalName, definition.InternalName, StringComparison.OrdinalIgnoreCase)); + + if (existingIndex >= 0) + result[key][existingIndex] = definition; + else + result[key].Add(definition); + } + } + } + + /// + /// Parses a comma-separated list of IDs and ranges into a flat list of uint values. + /// Supports: "5", "1,2,3", "0-10", "0-5,73,100-105". + /// + private static List ParseIdList(string idString, string filePath) + { + var ids = new List(); + + foreach (var segment in idString.Split(',')) + { + var trimmed = segment.Trim(); + if (string.IsNullOrEmpty(trimmed)) + continue; + + var dashIndex = trimmed.IndexOf('-'); + if (dashIndex > 0 && dashIndex < trimmed.Length - 1) + { + // Range: "start-end". + var startStr = trimmed.Substring(0, dashIndex).Trim(); + var endStr = trimmed.Substring(dashIndex + 1).Trim(); + + if (uint.TryParse(startStr, out uint rangeStart) && uint.TryParse(endStr, out uint rangeEnd)) + { + if (rangeEnd < rangeStart) + { + logger.Warn("Invalid range '{0}' (end < start) in {1}", trimmed, filePath); + continue; + } + + if (rangeEnd - rangeStart > 1000) + { + logger.Warn("Range '{0}' is too large (>1000 entries) in {1}", trimmed, filePath); + continue; + } + + for (uint i = rangeStart; i <= rangeEnd; i++) + { + if (!ids.Contains(i)) + ids.Add(i); + } + } + else + { + logger.Warn("Invalid range value '{0}' in {1}", trimmed, filePath); + } + } + else + { + // Single ID. + if (uint.TryParse(trimmed, out uint singleId)) + { + if (!ids.Contains(singleId)) + ids.Add(singleId); + } + else + { + logger.Warn("Invalid ID value '{0}' in {1}", trimmed, filePath); + } + } + } + + return ids; + } + + /// + /// Parses a single <property> XML node into a . + /// Returns null if the node is malformed beyond recovery. + /// + private static LuaPropertyDefinition ParsePropertyNode(XmlNode propNode, string filePath) + { + var definition = new LuaPropertyDefinition(); + + // Required: internalName. + definition.InternalName = propNode.Attributes?["internalName"]?.Value?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(definition.InternalName)) + { + logger.Warn("Property missing 'internalName' attribute in {0}", filePath); + return null; + } + + // Required: displayName. + definition.DisplayName = propNode.Attributes?["displayName"]?.Value?.Trim() ?? definition.InternalName; + + // Optional: description. + definition.Description = TextExtensions.SingleLineToMultiLine(propNode.Attributes?["description"]?.Value?.Trim() ?? string.Empty); + + // Optional: category + definition.Category = propNode.Attributes?["category"]?.Value?.Trim() ?? string.Empty; + + // Required: type. + var typeStr = propNode.Attributes?["type"]?.Value?.Trim() ?? string.Empty; + if (!TryParsePropertyType(typeStr, out var propertyType)) + { + logger.Warn("Property '{0}' has invalid type '{1}' in {2}, defaulting to float", definition.InternalName, typeStr, filePath); + propertyType = LuaPropertyType.Float; + } + definition.Type = propertyType; + + // Optional: hasAlpha (only meaningful for Color properties). + var hasAlphaStr = propNode.Attributes?["hasAlpha"]?.Value?.Trim() ?? string.Empty; + if (bool.TryParse(hasAlphaStr, out var hasAlpha)) + definition.HasAlpha = hasAlpha; + + // Optional: enum entries (only meaningful for Enum type). + if (propertyType == LuaPropertyType.Enum) + { + var entriesAttr = propNode.Attributes?["entries"]?.Value?.Trim() ?? string.Empty; + if (!string.IsNullOrEmpty(entriesAttr)) + { + definition.EnumValues = entriesAttr.Split(',') + .Select(e => e.Trim()) + .Where(e => !string.IsNullOrEmpty(e)) + .ToList(); + } + else + { + foreach (XmlNode entryNode in propNode.SelectNodes("entry")) + { + var entryVal = (entryNode.Attributes?["value"]?.Value + ?? entryNode.Attributes?["name"]?.Value)?.Trim() + ?? string.Empty; + + if (!string.IsNullOrEmpty(entryVal)) + definition.EnumValues.Add(entryVal); + } + } + + if (definition.EnumValues.Count == 0) + logger.Warn("Enum property '{0}' has no entries defined in {1}", definition.InternalName, filePath); + } + + // Optional: default value (accept both "defaultValue" and "default" attribute names). + var defaultStr = (propNode.Attributes?["defaultValue"]?.Value + ?? propNode.Attributes?["default"]?.Value)?.Trim() + ?? string.Empty; + + // For enum: allow the default to be an entry name; convert to 0-based integer. + if (propertyType == LuaPropertyType.Enum && !string.IsNullOrEmpty(defaultStr)) + { + if (!int.TryParse(defaultStr, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out _)) + { + int nameIdx = definition.EnumValues.FindIndex(e => string.Equals(e, defaultStr, StringComparison.OrdinalIgnoreCase)); + defaultStr = nameIdx >= 0 ? LuaValueParser.BoxInt(nameIdx) : string.Empty; // Fall through to type default below. + } + } + + if (!string.IsNullOrEmpty(defaultStr)) + { + if (LuaValueParser.ValidateBoxedValue(propertyType, defaultStr)) + { + definition.DefaultValue = defaultStr; + } + else + { + definition.DefaultValue = LuaValueParser.GetDefaultBoxedValue(propertyType); + logger.Warn("Property '{0}' has mismatched default value '{1}' for type {2} in {3}, using type default", + definition.InternalName, defaultStr, propertyType, filePath); + } + } + else + { + definition.DefaultValue = LuaValueParser.GetDefaultBoxedValue(propertyType); + } + + return definition; + } + + /// + /// Parses a property type string from XML (case-insensitive). + /// + private static bool TryParsePropertyType(string typeStr, out LuaPropertyType result) + { + if (Enum.TryParse(typeStr, ignoreCase: true, out result)) + return true; + + // Try common aliases + switch (typeStr?.ToLowerInvariant()) + { + case "boolean": result = LuaPropertyType.Bool; return true; + case "integer": result = LuaPropertyType.Int; return true; + case "number": result = LuaPropertyType.Float; return true; + case "vector2": result = LuaPropertyType.Vec2; return true; + case "vector3": result = LuaPropertyType.Vec3; return true; + + default: return false; + } + } + } +} diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyContainer.cs b/TombLib/TombLib/LuaProperties/LuaPropertyContainer.cs new file mode 100644 index 0000000000..2da2a56441 --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyContainer.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +// Property container that holds Lua property values in boxed (Lua text) format. +// Used for both Level 1 (global per-object-type, stored in wad2 files) and Level 2 +// (per-instance, stored in prj2 files). Values are stored in their boxed Lua representatio +// so they can be written directly to Lua script blobs during level compilation. + +namespace TombLib.LuaProperties +{ + /// + /// A dictionary-like container that stores Lua property values by their internal name. + /// All values are stored as boxed Lua strings (e.g. "true", "42", "TEN.Vec3(1,2,3)"). + /// This container is serialized into wad2/prj2 files and written to level Lua scripts. + /// + [Serializable] + public class LuaPropertyContainer + { + /// + /// Internal storage: property internal name → boxed Lua value string. + /// + private readonly Dictionary _properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets the number of properties in this container. + /// + public int Count => _properties.Count; + + /// + /// Returns true if this container has any properties set. + /// + public bool HasProperties => _properties.Count > 0; + + /// + /// Gets or sets a boxed Lua value by its internal property name. + /// Setting to null removes the property. + /// + public string this[string internalName] + { + get => _properties.TryGetValue(internalName, out var value) ? value : null; + set + { + if (value == null) + _properties.Remove(internalName); + else + _properties[internalName] = value; + } + } + + /// + /// Gets the boxed Lua value for the given property name. + /// Returns null if not set. + /// + public string GetValue(string internalName) + { + return _properties.TryGetValue(internalName, out var value) ? value : null; + } + + /// + /// Sets a boxed Lua value for the given property name. + /// If the value is null or empty, the property is removed. + /// + public void SetValue(string internalName, string boxedValue) + { + if (string.IsNullOrEmpty(boxedValue)) + _properties.Remove(internalName); + else + _properties[internalName] = boxedValue; + } + + /// + /// Removes a property by its internal name. + /// + public bool Remove(string internalName) + { + return _properties.Remove(internalName); + } + + /// + /// Clears all properties. + /// + public void Clear() + { + _properties.Clear(); + } + + /// + /// Returns true if the property is set in this container. + /// + public bool Contains(string internalName) + { + return _properties.ContainsKey(internalName); + } + + /// + /// Enumerates all property name/value pairs. + /// + public IEnumerable> GetAll() + { + return _properties.AsEnumerable(); + } + + /// + /// Creates a deep copy of this container. + /// + public LuaPropertyContainer Clone() + { + var clone = new LuaPropertyContainer(); + + foreach (var kvp in _properties) + clone._properties[kvp.Key] = kvp.Value; + + return clone; + } + } +} diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs b/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs new file mode 100644 index 0000000000..354477eeba --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +// Represents a single property definition loaded from an XML property catalog. +// Used to describe what properties are available for a given object type and +// to provide metadata for the property grid UI (name, description, type, default). + +namespace TombLib.LuaProperties +{ + /// + /// Defines a single property that can appear in the Lua property grid. + /// Loaded from XML catalog files in "Catalogs/TEN Property Catalogs". + /// + public class LuaPropertyDefinition + { + /// + /// User-friendly display name shown in the property grid's label column. + /// Example: "Melee damage" + /// + public string DisplayName { get; set; } = string.Empty; + + /// + /// Internal Lua API property name used in SetProperty/GetProperty calls. + /// Example: "meleeDamage" + /// + public string InternalName { get; set; } = string.Empty; + + /// + /// Tooltip description for the property. + /// Example: "A damage value applied in melee combat" + /// + public string Description { get; set; } = string.Empty; + + /// + /// The Lua value type of this property. + /// + public LuaPropertyType Type { get; set; } = LuaPropertyType.Float; + + /// + /// Optional category grouping for the property grid. + /// Properties without a category are displayed at the top level. + /// Example: "Battle", "Logic", "Physics" + /// + public string Category { get; set; } = string.Empty; + + /// + /// Default value in boxed Lua format. + /// Must be compatible with the declared Type. + /// + public string DefaultValue { get; set; } = string.Empty; + + /// + /// For Color properties: whether the alpha channel is editable. + /// When false, the alpha field is hidden in the property grid. + /// Controlled by the "hasAlpha" attribute in XML catalogs. + /// + public bool HasAlpha { get; set; } = false; + + /// + /// For Enum properties: the ordered list of entry names. + /// Index 0 corresponds to Lua integer value 0, index 1 to 1, etc. + /// Populated from the entries XML attribute or child <entry> nodes. + /// + public List EnumValues { get; set; } = new List(); + + /// + /// Returns true if the definition has all required fields filled in. + /// + public bool IsValid => !string.IsNullOrWhiteSpace(InternalName) && !string.IsNullOrWhiteSpace(DisplayName); + + public override string ToString() => $"{DisplayName} ({InternalName}: {Type})"; + } +} diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyScriptBuilder.cs b/TombLib/TombLib/LuaProperties/LuaPropertyScriptBuilder.cs new file mode 100644 index 0000000000..b6b8cee02b --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyScriptBuilder.cs @@ -0,0 +1,191 @@ +using System.Collections.Generic; +using System.Text; +using TombLib.Utils; + +// Generates Lua API call strings for property assignment during level compilation. +// Produces calls to TombEngine's property API: +// +// Objects.SetMoveableProperty(ObjID.XXX, "propName", value) -- Level 1 (global) +// Objects.SetStaticProperty(slotID, "propName", value) -- Level 1 (global) +// GetMoveableByName("luaName"):SetProperty("propName", value) -- Level 2 (instance) +// GetStaticByName("luaName"):SetProperty("propName", value) -- Level 2 (instance) +// +// Both layers are written; the engine handles override resolution internally. + +namespace TombLib.LuaProperties +{ + /// + /// Builds Lua script text for setting properties during level compilation. + /// The generated Lua code is embedded in the compiled level file and executed + /// by TombEngine during level load. + /// + public static class LuaPropertyScriptBuilder + { + // TEN Lua API function names + private const string SetMoveablePropertyFunc = "TEN.Objects.SetMoveableProperty"; + private const string SetStaticPropertyFunc = "TEN.Objects.SetStaticProperty"; + private const string GetMoveableByNameFunc = "TEN.Objects.GetMoveableByName"; + private const string GetStaticByNameFunc = "TEN.Objects.GetStaticByName"; + + /// + /// Generates a Level 1 (global) Lua property assignment for a moveable object type. + /// Output: TEN.Objects.SetMoveableProperty(TEN.Objects.ObjID.SLOT_NAME, "propName", value) + /// + /// The TEN slot name (e.g. "BADDY1"). + /// The internal property name. + /// The already-boxed Lua value string. + public static string BuildGlobalMoveableProperty(string slotName, string propertyName, string boxedValue) + { + return SetMoveablePropertyFunc + LuaSyntax.BracketOpen + + LuaSyntax.ObjectIDPrefix + LuaSyntax.Splitter + slotName + + LuaSyntax.Separator + LuaSyntax.Space + + TextExtensions.Quote(propertyName) + + LuaSyntax.Separator + LuaSyntax.Space + + boxedValue + + LuaSyntax.BracketClose; + } + + /// + /// Generates a Level 1 (global) Lua property assignment for a static object type. + /// Output: TEN.Objects.SetStaticProperty(slotId, "propName", value) + /// + /// The static slot numeric ID. + /// The internal property name. + /// The already-boxed Lua value string. + public static string BuildGlobalStaticProperty(uint slotId, string propertyName, string boxedValue) + { + return SetStaticPropertyFunc + LuaSyntax.BracketOpen + + slotId.ToString() + + LuaSyntax.Separator + LuaSyntax.Space + + TextExtensions.Quote(propertyName) + + LuaSyntax.Separator + LuaSyntax.Space + + boxedValue + + LuaSyntax.BracketClose; + } + + /// + /// Generates a Level 2 (instance) Lua property assignment for a moveable. + /// Output: TEN.Objects.GetMoveableByName("luaName"):SetProperty("propName", value) + /// + /// The instance's LuaName identifier. + /// The internal property name. + /// The already-boxed Lua value string. + public static string BuildInstanceMoveableProperty(string luaName, string propertyName, string boxedValue) + { + return GetMoveableByNameFunc + LuaSyntax.BracketOpen + + TextExtensions.Quote(luaName) + + LuaSyntax.BracketClose + + ":SetProperty" + LuaSyntax.BracketOpen + + TextExtensions.Quote(propertyName) + + LuaSyntax.Separator + LuaSyntax.Space + + boxedValue + + LuaSyntax.BracketClose; + } + + /// + /// Generates a Level 2 (instance) Lua property assignment for a static. + /// Output: TEN.Objects.GetStaticByName("luaName"):SetProperty("propName", value) + /// + /// The instance's LuaName identifier. + /// The internal property name. + /// The already-boxed Lua value string. + public static string BuildInstanceStaticProperty(string luaName, string propertyName, string boxedValue) + { + return GetStaticByNameFunc + LuaSyntax.BracketOpen + + TextExtensions.Quote(luaName) + + LuaSyntax.BracketClose + + ":SetProperty" + LuaSyntax.BracketOpen + + TextExtensions.Quote(propertyName) + + LuaSyntax.Separator + LuaSyntax.Space + + boxedValue + + LuaSyntax.BracketClose; + } + + /// + /// Generates a complete Lua script block for all Level 1 and Level 2 properties. + /// The output is a series of API calls separated by newlines. + /// + /// Level 1 moveable properties: slot name → container. + /// Level 1 static properties: slot ID → container. + /// Level 2 moveable properties: lua name → container. + /// Level 2 static properties: lua name → container. + public static KeyValuePair BuildFullPropertyScript( + Dictionary globalMoveableProperties, + Dictionary globalStaticProperties, + Dictionary instanceMoveableProperties, + Dictionary instanceStaticProperties) + { + int propertyCount = 0; + var sb = new StringBuilder(); + + sb.AppendLine("-- Auto-generated property assignment script"); + sb.AppendLine(); + sb.AppendLine("-- Level 1: Global object type properties"); + sb.AppendLine(); + + // Level 1: Global Moveable properties + if (globalMoveableProperties != null) + { + foreach (var kvp in globalMoveableProperties) + { + foreach (var prop in kvp.Value.GetAll()) + { + sb.AppendLine(BuildGlobalMoveableProperty(kvp.Key, prop.Key, prop.Value)); + propertyCount++; + } + } + } + + // Level 1: Global Static properties + if (globalStaticProperties != null) + { + foreach (var kvp in globalStaticProperties) + { + foreach (var prop in kvp.Value.GetAll()) + { + sb.AppendLine(BuildGlobalStaticProperty(kvp.Key, prop.Key, prop.Value)); + propertyCount++; + } + } + } + + sb.AppendLine(); + sb.AppendLine("-- Level 2: Instance properties"); + sb.AppendLine(); + + // Level 2: Instance Moveable properties + if (instanceMoveableProperties != null) + { + foreach (var kvp in instanceMoveableProperties) + { + if (string.IsNullOrEmpty(kvp.Key)) + continue; // Instance must have a LuaName + + foreach (var prop in kvp.Value.GetAll()) + { + sb.AppendLine(BuildInstanceMoveableProperty(kvp.Key, prop.Key, prop.Value)); + propertyCount++; + } + } + } + + // Level 2: Instance Static properties + if (instanceStaticProperties != null) + { + foreach (var kvp in instanceStaticProperties) + { + if (string.IsNullOrEmpty(kvp.Key)) + continue; + + foreach (var prop in kvp.Value.GetAll()) + { + sb.AppendLine(BuildInstanceStaticProperty(kvp.Key, prop.Key, prop.Value)); + propertyCount++; + } + } + } + + return new KeyValuePair(propertyCount, sb.ToString()); + } + } +} diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyType.cs b/TombLib/TombLib/LuaProperties/LuaPropertyType.cs new file mode 100644 index 0000000000..383710908e --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyType.cs @@ -0,0 +1,19 @@ +// Defines supported Lua property value types for TombEngine property system. +// These correspond to TEN Lua API types: bool, int, float, string, Vec2, Vec3, Rotation, Color, Time, Enum. + +namespace TombLib.LuaProperties +{ + public enum LuaPropertyType + { + Bool, + Int, + Float, + String, + Vec2, + Vec3, + Rotation, + Color, + Time, + Enum + } +} diff --git a/TombLib/TombLib/LuaProperties/LuaValueParser.cs b/TombLib/TombLib/LuaProperties/LuaValueParser.cs new file mode 100644 index 0000000000..e5a2806bf4 --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaValueParser.cs @@ -0,0 +1,463 @@ +using System; +using System.Globalization; +using System.Linq; +using NLog; +using TombLib.Utils; + +// Shared Lua value boxing/unboxing abstraction. +// Used by both the visual scripting ArgumentEditor and the property grid system. + +namespace TombLib.LuaProperties +{ + /// + /// Provides static methods for boxing (C# → Lua string) and unboxing (Lua string → C#) + /// of property values in TombEngine's Lua format. + /// All methods are defensive and will not throw on malformed input. + /// + public static class LuaValueParser + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + // Culture-invariant formatting to prevent locale issues with decimals. + private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; + + #region Boxing (C# values → Lua text) + + public static string BoxBool(bool value) => value ? "true" : "false"; + + public static string BoxInt(int value) => value.ToString(Inv); + + public static string BoxFloat(float value) => value.ToString(Inv); + + public static string BoxString(string value) => TextExtensions.Quote(TextExtensions.EscapeQuotes(value ?? string.Empty)); + + public static string BoxVec2(float x, float y) + { + return LuaSyntax.Vec2TypePrefix + LuaSyntax.BracketOpen + + x.ToString(Inv) + LuaSyntax.Separator + + y.ToString(Inv) + LuaSyntax.BracketClose; + } + + public static string BoxVec3(float x, float y, float z) + { + return LuaSyntax.Vec3TypePrefix + LuaSyntax.BracketOpen + + x.ToString(Inv) + LuaSyntax.Separator + + y.ToString(Inv) + LuaSyntax.Separator + + z.ToString(Inv) + LuaSyntax.BracketClose; + } + + public static string BoxRotation(float x, float y, float z) + { + return LuaSyntax.RotationTypePrefix + LuaSyntax.BracketOpen + + x.ToString(Inv) + LuaSyntax.Separator + + y.ToString(Inv) + LuaSyntax.Separator + + z.ToString(Inv) + LuaSyntax.BracketClose; + } + + public static string BoxColor(byte r, byte g, byte b, byte? a = null) + { + var result = LuaSyntax.ColorTypePrefix + LuaSyntax.BracketOpen + + r.ToString(Inv) + LuaSyntax.Separator + + g.ToString(Inv) + LuaSyntax.Separator + + b.ToString(Inv); + + if (a.HasValue) + result += LuaSyntax.Separator + a.Value.ToString(Inv); + + result += LuaSyntax.BracketClose; + return result; + } + + public static string BoxTime(int hours, int minutes, int seconds, int centiseconds) + { + return LuaSyntax.TimeTypePrefix + LuaSyntax.BracketOpen + LuaSyntax.TableOpen + + hours.ToString(Inv) + LuaSyntax.Separator + + minutes.ToString(Inv) + LuaSyntax.Separator + + seconds.ToString(Inv) + LuaSyntax.Separator + + centiseconds.ToString(Inv) + + LuaSyntax.TableClose + LuaSyntax.BracketClose; + } + + #endregion + + #region Unboxing (Lua text → C# values) + + public static bool UnboxBool(string source, bool defaultValue = false) + { + if (string.IsNullOrWhiteSpace(source)) + return defaultValue; + + source = source.Trim(); + + if (bool.TryParse(source, out bool boolResult)) + return boolResult; + + if (float.TryParse(source, NumberStyles.Float, Inv, out float floatResult)) + return floatResult != 0.0f; + + return defaultValue; + } + + public static int UnboxInt(string source, int defaultValue = 0) + { + if (string.IsNullOrWhiteSpace(source)) + return defaultValue; + + source = source.Trim(); + + // Try int first, then float (truncated). + if (int.TryParse(source, NumberStyles.Integer, Inv, out int intResult)) + return intResult; + + if (float.TryParse(source, NumberStyles.Float, Inv, out float floatResult)) + return (int)floatResult; + + if (bool.TryParse(source, out bool boolResult)) + return boolResult ? 1 : 0; + + return defaultValue; + } + + public static float UnboxFloat(string source, float defaultValue = 0.0f) + { + if (string.IsNullOrWhiteSpace(source)) + return defaultValue; + + source = source.Trim(); + + if (float.TryParse(source, NumberStyles.Float, Inv, out float result)) + return result; + + if (bool.TryParse(source, out bool boolResult)) + return boolResult ? 1.0f : 0.0f; + + return defaultValue; + } + + public static string UnboxString(string source, string defaultValue = "") + { + if (source == null) + return defaultValue; + + return TextExtensions.UnescapeQuotes(TextExtensions.Unquote(source)); + } + + public static float[] UnboxVec2(string source) + { + var defaults = new float[] { 0, 0 }; + if (string.IsNullOrWhiteSpace(source)) + return defaults; + + source = StripTypePrefix(source, LuaSyntax.Vec2TypePrefix); + var components = SplitComponents(source); + + if (components.Length < 2) + { + logger.Warn("UnboxVec2: Expected 2 components, got " + components.Length); + return defaults; + } + + return new float[] + { + ParseFloat(components[0]), + ParseFloat(components[1]) + }; + } + + public static float[] UnboxVec3(string source) + { + var defaults = new float[] { 0, 0, 0 }; + if (string.IsNullOrWhiteSpace(source)) + return defaults; + + source = StripTypePrefix(source, LuaSyntax.Vec3TypePrefix); + var components = SplitComponents(source); + + if (components.Length < 3) + { + logger.Warn("UnboxVec3: Expected 3 components, got " + components.Length); + return defaults; + } + + return new float[] + { + ParseFloat(components[0]), + ParseFloat(components[1]), + ParseFloat(components[2]) + }; + } + + public static float[] UnboxRotation(string source) + { + var defaults = new float[] { 0, 0, 0 }; + if (string.IsNullOrWhiteSpace(source)) + return defaults; + + source = StripTypePrefix(source, LuaSyntax.RotationTypePrefix); + var components = SplitComponents(source); + + if (components.Length < 3) + { + logger.Warn("UnboxRotation: Expected 3 components, got " + components.Length); + return defaults; + } + + return new float[] + { + ClampAngle(ParseFloat(components[0])), + ClampAngle(ParseFloat(components[1])), + ClampAngle(ParseFloat(components[2])) + }; + } + + public static byte[] UnboxColor(string source) + { + var defaults = new byte[] { 0, 0, 0, 255 }; + if (string.IsNullOrWhiteSpace(source)) + return defaults; + + source = StripTypePrefix(source, LuaSyntax.ColorTypePrefix); + var components = SplitComponents(source); + + if (components.Length < 3) + { + logger.Warn("UnboxColor: Expected at least 3 components, got " + components.Length); + return defaults; + } + + byte r = ClampByte(ParseFloat(components[0])); + byte g = ClampByte(ParseFloat(components[1])); + byte b = ClampByte(ParseFloat(components[2])); + byte a = components.Length > 3 ? ClampByte(ParseFloat(components[3])) : (byte)255; + + return new byte[] { r, g, b, a }; + } + + public static int[] UnboxTime(string source) + { + var defaults = new int[] { 0, 0, 0, 0 }; + if (string.IsNullOrWhiteSpace(source)) + return defaults; + + source = StripTypePrefix(source, LuaSyntax.TimeTypePrefix); + source = source.Replace(LuaSyntax.TableOpen, "").Replace(LuaSyntax.TableClose, ""); + + var components = SplitComponents(source); + + if (components.Length < 4) + { + logger.Warn("UnboxTime: Expected 4 components, got " + components.Length); + return defaults; + } + + return new int[] + { + (int)ParseFloat(components[0]), + (int)ParseFloat(components[1]), + (int)ParseFloat(components[2]), + (int)ParseFloat(components[3]) + }; + } + + #endregion + + #region Generic boxing by type + + /// + /// Boxes a property value to its Lua text representation based on the declared type. + /// Returns a default value string if the input is null or empty. + /// + public static string BoxByType(LuaPropertyType type, object value) + { + try + { + switch (type) + { + case LuaPropertyType.Bool: + return BoxBool(value is bool b ? b : false); + + case LuaPropertyType.Int: + return BoxInt(value is int i ? i : 0); + + case LuaPropertyType.Float: + return BoxFloat(value is float f ? f : 0.0f); + + case LuaPropertyType.String: + return BoxString(value as string ?? string.Empty); + + case LuaPropertyType.Vec2: + if (value is float[] v2 && v2.Length >= 2) + return BoxVec2(v2[0], v2[1]); + return BoxVec2(0, 0); + + case LuaPropertyType.Vec3: + if (value is float[] v3 && v3.Length >= 3) + return BoxVec3(v3[0], v3[1], v3[2]); + return BoxVec3(0, 0, 0); + + case LuaPropertyType.Rotation: + if (value is float[] rot && rot.Length >= 3) + return BoxRotation(rot[0], rot[1], rot[2]); + return BoxRotation(0, 0, 0); + + case LuaPropertyType.Color: + if (value is byte[] c && c.Length >= 3) + return c.Length > 3 && c[3] != 255 ? BoxColor(c[0], c[1], c[2], c[3]) : BoxColor(c[0], c[1], c[2]); + return BoxColor(0, 0, 0); + + case LuaPropertyType.Time: + if (value is int[] t && t.Length >= 4) + return BoxTime(t[0], t[1], t[2], t[3]); + return BoxTime(0, 0, 0, 0); + + case LuaPropertyType.Enum: + return BoxInt(value is int enumVal ? enumVal : 0); // Enum values are stored as 0-based integers. + + default: + logger.Warn("BoxByType: Unknown property type {0}", type); + return string.Empty; + } + } + catch (Exception ex) + { + logger.Error(ex, "BoxByType failed for type {0}", type); + return GetDefaultBoxedValue(type); + } + } + + /// + /// Returns the default boxed Lua string for a given property type. + /// + public static string GetDefaultBoxedValue(LuaPropertyType type) + { + switch (type) + { + case LuaPropertyType.Bool: return BoxBool(false); + case LuaPropertyType.Int: return BoxInt(0); + case LuaPropertyType.Float: return BoxFloat(0); + case LuaPropertyType.String: return BoxString(string.Empty); + case LuaPropertyType.Vec2: return BoxVec2(0, 0); + case LuaPropertyType.Vec3: return BoxVec3(0, 0, 0); + case LuaPropertyType.Rotation: return BoxRotation(0, 0, 0); + case LuaPropertyType.Color: return BoxColor(0, 0, 0); + case LuaPropertyType.Time: return BoxTime(0, 0, 0, 0); + case LuaPropertyType.Enum: return BoxInt(0); + default: return string.Empty; + } + } + + /// + /// Validates that a boxed Lua string is compatible with the declared property type. + /// Returns true if valid, false if malformed. + /// + public static bool ValidateBoxedValue(LuaPropertyType type, string boxedValue) + { + if (string.IsNullOrWhiteSpace(boxedValue)) + return false; + + try + { + switch (type) + { + case LuaPropertyType.Bool: + UnboxBool(boxedValue); + return true; + + case LuaPropertyType.Int: + return int.TryParse(boxedValue.Trim(), NumberStyles.Integer, Inv, out _) || + float.TryParse(boxedValue.Trim(), NumberStyles.Float, Inv, out _); + + case LuaPropertyType.Float: + return float.TryParse(boxedValue.Trim(), NumberStyles.Float, Inv, out _); + + case LuaPropertyType.String: + return boxedValue.StartsWith(TextExtensions.QuoteChar) && + boxedValue.EndsWith(TextExtensions.QuoteChar); + + case LuaPropertyType.Vec2: + return boxedValue.StartsWith(LuaSyntax.Vec2TypePrefix); + + case LuaPropertyType.Vec3: + return boxedValue.StartsWith(LuaSyntax.Vec3TypePrefix); + + case LuaPropertyType.Rotation: + return boxedValue.StartsWith(LuaSyntax.RotationTypePrefix); + + case LuaPropertyType.Color: + return boxedValue.StartsWith(LuaSyntax.ColorTypePrefix); + + case LuaPropertyType.Time: + return boxedValue.StartsWith(LuaSyntax.TimeTypePrefix); + + case LuaPropertyType.Enum: + return int.TryParse(boxedValue.Trim(), NumberStyles.Integer, Inv, out int enumIndex) && enumIndex >= 0; + + default: + return false; + } + } + catch + { + return false; + } + } + + #endregion + + #region Internal helpers + + /// + /// Strips a type prefix and surrounding brackets from a Lua constructor string. + /// E.g. "TEN.Vec3(1,2,3)" → "1,2,3" + /// + public static string StripTypePrefix(string source, string prefix) + { + if (source.StartsWith(prefix + LuaSyntax.BracketOpen) && source.EndsWith(LuaSyntax.BracketClose)) + return source.Substring(prefix.Length + 1, source.Length - prefix.Length - 2); + + return source; + } + + /// + /// Splits a comma-separated value string into trimmed components. + /// Reused by both ArgumentEditor and property grid system. + /// + public static float[] SplitAndParseFloats(string source) + { + return source.Split(new string[] { LuaSyntax.Separator }, StringSplitOptions.None) + .Select(x => ParseFloat(x.Trim())) + .ToArray(); + } + + private static string[] SplitComponents(string source) + { + return source.Split(new string[] { LuaSyntax.Separator }, StringSplitOptions.None) + .Select(x => x.Trim()) + .ToArray(); + } + + private static float ParseFloat(string source) + { + if (float.TryParse(source, NumberStyles.Float, Inv, out float result)) + return result; + + return 0.0f; + } + + private static float ClampAngle(float value) + { + // Normalize to 0-360 range. + value = value % 360.0f; + if (value < 0) + value += 360.0f; + return value; + } + + private static byte ClampByte(float value) + { + return (byte)Math.Max(0, Math.Min(255, (int)value)); + } + + #endregion + } +} diff --git a/TombLib/TombLib/TombLib.csproj b/TombLib/TombLib/TombLib.csproj index 599b3f3c9c..ed12efd17a 100644 --- a/TombLib/TombLib/TombLib.csproj +++ b/TombLib/TombLib/TombLib.csproj @@ -479,6 +479,12 @@ Always + + Always + + + Always + diff --git a/TombLib/TombLib/Utils/ScriptingUtils.cs b/TombLib/TombLib/Utils/ScriptingUtils.cs index e0b0daf77f..a27881539b 100644 --- a/TombLib/TombLib/Utils/ScriptingUtils.cs +++ b/TombLib/TombLib/Utils/ScriptingUtils.cs @@ -32,6 +32,7 @@ public static class LuaSyntax public const string TimeTypePrefix = "TEN.Time"; public const string Vec2TypePrefix = "TEN.Vec2"; public const string Vec3TypePrefix = "TEN.Vec3"; + public const string RotationTypePrefix = "TEN.Rotation"; public const string ObjectIDPrefix = "TEN.Objects.ObjID"; public const string ReservedFunctionPrefix = "__"; diff --git a/TombLib/TombLib/Wad/Wad2Chunks.cs b/TombLib/TombLib/Wad/Wad2Chunks.cs index 4d54eb0a1b..d7d304a946 100644 --- a/TombLib/TombLib/Wad/Wad2Chunks.cs +++ b/TombLib/TombLib/Wad/Wad2Chunks.cs @@ -151,6 +151,11 @@ public static class Wad2Chunks /******/public static readonly ChunkId StaticLightIntensity = ChunkId.FromString("W2StaticLightI"); /******/public static readonly ChunkId StaticShatter = ChunkId.FromString("W2StaticShatter"); /******/public static readonly ChunkId StaticShatterSound = ChunkId.FromString("W2StaticShatterSound"); + // Lua property containers (Level 1 global properties) + public static readonly ChunkId LuaProperties = ChunkId.FromString("W2LuaProps"); + /**/public static readonly ChunkId LuaProperty = ChunkId.FromString("W2LuaProp"); + /****/public static readonly ChunkId LuaPropertyName = ChunkId.FromString("W2LuaPrN"); + /****/public static readonly ChunkId LuaPropertyValue = ChunkId.FromString("W2LuaPrV"); /**/public static readonly ChunkId AnimatedTextureSets = ChunkId.FromString("W2AnimatedTextureSets"); /******/public static readonly ChunkId AnimatedTextureSet = ChunkId.FromString("W2AnimatedTextureSet"); /**********/public static readonly ChunkId AnimatedTextureSetName = ChunkId.FromString("W2AnimatedTextureSetName"); diff --git a/TombLib/TombLib/Wad/Wad2Loader.cs b/TombLib/TombLib/Wad/Wad2Loader.cs index 0210505d51..b82092c18a 100644 --- a/TombLib/TombLib/Wad/Wad2Loader.cs +++ b/TombLib/TombLib/Wad/Wad2Loader.cs @@ -911,6 +911,32 @@ private static bool LoadMoveables(ChunkReader chunkIO, ChunkId idOuter, Wad2 wad mov.Animations.Add(animation); } + else if (id2 == Wad2Chunks.LuaProperties) + { + chunkIO.ReadChunks((id3, chunkSize3) => + { + if (id3 != Wad2Chunks.LuaProperty) + return false; + + string name = null; + string value = null; + chunkIO.ReadChunks((id4, chunkSize4) => + { + if (id4 == Wad2Chunks.LuaPropertyName) + name = chunkIO.ReadChunkString(chunkSize4); + else if (id4 == Wad2Chunks.LuaPropertyValue) + value = chunkIO.ReadChunkString(chunkSize4); + else + return false; + return true; + }); + + if (!string.IsNullOrEmpty(name) && value != null) + mov.LuaProperties.SetValue(name, value); + + return true; + }); + } else { return false; @@ -1010,6 +1036,32 @@ private static bool LoadStatics(ChunkReader chunkIO, ChunkId idOuter, Wad2 wad, }); s.Lights.Add(light); } + else if (id2 == Wad2Chunks.LuaProperties) + { + chunkIO.ReadChunks((id3, chunkSize3) => + { + if (id3 != Wad2Chunks.LuaProperty) + return false; + + string name = null; + string value = null; + chunkIO.ReadChunks((id4, chunkSize4) => + { + if (id4 == Wad2Chunks.LuaPropertyName) + name = chunkIO.ReadChunkString(chunkSize4); + else if (id4 == Wad2Chunks.LuaPropertyValue) + value = chunkIO.ReadChunkString(chunkSize4); + else + return false; + return true; + }); + + if (!string.IsNullOrEmpty(name) && value != null) + s.LuaProperties.SetValue(name, value); + + return true; + }); + } else { return false; diff --git a/TombLib/TombLib/Wad/Wad2Writer.cs b/TombLib/TombLib/Wad/Wad2Writer.cs index 4e40292c14..1c51e60f56 100644 --- a/TombLib/TombLib/Wad/Wad2Writer.cs +++ b/TombLib/TombLib/Wad/Wad2Writer.cs @@ -440,6 +440,9 @@ private static void WriteMoveables(ChunkWriter chunkIO, Wad2 wad, List chunkIO.WriteChunkVector3(Wad2Chunks.MeshBoundingBoxMin, s.CollisionBox.Minimum); chunkIO.WriteChunkVector3(Wad2Chunks.MeshBoundingBoxMax, s.CollisionBox.Maximum); }); + + // Write Lua properties (Level 1) + WriteLuaProperties(chunkIO, s.LuaProperties); }); } }); @@ -507,5 +513,24 @@ public static void WriteMetadata(ChunkWriter chunkIO, Wad2 wad) chunkIO.WriteChunkString(Wad2Chunks.UserNotes, wad.UserNotes); }); } + + private static void WriteLuaProperties(ChunkWriter chunkIO, LuaProperties.LuaPropertyContainer container) + { + if (container == null || !container.HasProperties) + return; + + chunkIO.WriteChunkWithChildren(Wad2Chunks.LuaProperties, () => + { + var sortedProps = container.GetAll().OrderBy(p => p.Key).ToList(); + foreach (var prop in sortedProps) + { + chunkIO.WriteChunkWithChildren(Wad2Chunks.LuaProperty, () => + { + chunkIO.WriteChunkString(Wad2Chunks.LuaPropertyName, prop.Key); + chunkIO.WriteChunkString(Wad2Chunks.LuaPropertyValue, prop.Value); + }); + } + }); + } } } diff --git a/TombLib/TombLib/Wad/WadMoveable.cs b/TombLib/TombLib/Wad/WadMoveable.cs index 38c6dd57a8..3a97cb8e13 100644 --- a/TombLib/TombLib/Wad/WadMoveable.cs +++ b/TombLib/TombLib/Wad/WadMoveable.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using TombLib.LevelData; +using TombLib.LuaProperties; using TombLib.Utils; using TombLib.Wad.Catalog; @@ -78,6 +79,7 @@ public class WadMoveable : IWadObject public List Animations { get; } = new List(); //public WadBone Skeleton { get; set; } = new WadBone(); public List Bones { get; } = new List(); + public LuaPropertyContainer LuaProperties { get; set; } = new LuaPropertyContainer(); public WadMoveable(WadMoveableId id) { @@ -92,6 +94,7 @@ public WadMoveable Clone() { var mov = new WadMoveable(Id); mov.Skin = Skin?.Clone() ?? null; + mov.LuaProperties = LuaProperties.Clone(); foreach (var mesh in Meshes) mov.Meshes.Add(mesh.Clone()); foreach (var bone in Bones) diff --git a/TombLib/TombLib/Wad/WadStatic.cs b/TombLib/TombLib/Wad/WadStatic.cs index 21459186d3..5a7e67f4ae 100644 --- a/TombLib/TombLib/Wad/WadStatic.cs +++ b/TombLib/TombLib/Wad/WadStatic.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using TombLib.LevelData; +using TombLib.LuaProperties; using TombLib.Utils; using TombLib.Wad.Catalog; @@ -56,6 +57,8 @@ public WadStatic(WadStaticId id) public bool Shatter { get; set; } = false; public int ShatterSoundID { get; set; } = -1; + public LuaPropertyContainer LuaProperties { get; set; } = new LuaPropertyContainer(); + public string ToString(TRVersion.Game gameVersion) => Id.ToString(gameVersion.Native()); public override string ToString() => Id.ToString(); @@ -63,6 +66,7 @@ public WadStatic Clone() { WadStatic clone = (WadStatic)MemberwiseClone(); clone.Mesh = Mesh.Clone(); + clone.LuaProperties = LuaProperties.Clone(); return clone; } object ICloneable.Clone() => Clone(); diff --git a/WadTool/Controls/ContextMenus/BaseContextMenu.cs b/WadTool/Controls/ContextMenus/BaseContextMenu.cs index 7ec7e3c960..5f05916cc5 100644 --- a/WadTool/Controls/ContextMenus/BaseContextMenu.cs +++ b/WadTool/Controls/ContextMenus/BaseContextMenu.cs @@ -1,9 +1,4 @@ using DarkUI.Controls; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace WadTool.Controls.ContextMenus { diff --git a/WadTool/Controls/ContextMenus/MoveableContextMenu.cs b/WadTool/Controls/ContextMenus/MoveableContextMenu.cs index bb2fcc9f5a..ef1670601a 100644 --- a/WadTool/Controls/ContextMenus/MoveableContextMenu.cs +++ b/WadTool/Controls/ContextMenus/MoveableContextMenu.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; +using System.Windows.Forms; using TombLib.Wad; namespace WadTool.Controls.ContextMenus diff --git a/WadTool/Forms/FormLuaProperties.Designer.cs b/WadTool/Forms/FormLuaProperties.Designer.cs new file mode 100644 index 0000000000..d97a2e0e24 --- /dev/null +++ b/WadTool/Forms/FormLuaProperties.Designer.cs @@ -0,0 +1,167 @@ +namespace WadTool +{ + partial class FormLuaProperties + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing) + { + _elementHost?.Dispose(); + components?.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + panelButtons = new System.Windows.Forms.Panel(); + butReset = new DarkUI.Controls.DarkButton(); + butCancel = new DarkUI.Controls.DarkButton(); + butOK = new DarkUI.Controls.DarkButton(); + panelLeft = new System.Windows.Forms.Panel(); + lstObjects = new DarkUI.Controls.DarkListView(); + splitter = new System.Windows.Forms.Splitter(); + panelContent = new System.Windows.Forms.Panel(); + panelButtons.SuspendLayout(); + panelLeft.SuspendLayout(); + SuspendLayout(); + // + // panelButtons + // + panelButtons.Controls.Add(butReset); + panelButtons.Controls.Add(butCancel); + panelButtons.Controls.Add(butOK); + panelButtons.Dock = System.Windows.Forms.DockStyle.Bottom; + panelButtons.Location = new System.Drawing.Point(5, 449); + panelButtons.Name = "panelButtons"; + panelButtons.Padding = new System.Windows.Forms.Padding(6); + panelButtons.Size = new System.Drawing.Size(614, 30); + panelButtons.TabIndex = 0; + // + // butReset + // + butReset.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left; + butReset.Checked = false; + butReset.Location = new System.Drawing.Point(3, 7); + butReset.Name = "butReset"; + butReset.Size = new System.Drawing.Size(80, 23); + butReset.TabIndex = 2; + butReset.Text = "Reset All"; + butReset.Click += butReset_Click; + // + // butCancel + // + butCancel.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; + butCancel.Checked = false; + butCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + butCancel.Location = new System.Drawing.Point(534, 7); + butCancel.Name = "butCancel"; + butCancel.Size = new System.Drawing.Size(80, 23); + butCancel.TabIndex = 1; + butCancel.Text = "Cancel"; + butCancel.Click += butCancel_Click; + // + // butOK + // + butOK.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right; + butOK.Checked = false; + butOK.Location = new System.Drawing.Point(448, 7); + butOK.Name = "butOK"; + butOK.Size = new System.Drawing.Size(80, 23); + butOK.TabIndex = 0; + butOK.Text = "OK"; + butOK.Click += butOK_Click; + // + // panelLeft + // + panelLeft.Controls.Add(lstObjects); + panelLeft.Dock = System.Windows.Forms.DockStyle.Left; + panelLeft.Location = new System.Drawing.Point(5, 5); + panelLeft.Name = "panelLeft"; + panelLeft.Padding = new System.Windows.Forms.Padding(3); + panelLeft.Size = new System.Drawing.Size(200, 444); + panelLeft.TabIndex = 1; + // + // lstObjects + // + lstObjects.Dock = System.Windows.Forms.DockStyle.Fill; + lstObjects.Location = new System.Drawing.Point(3, 3); + lstObjects.Margin = new System.Windows.Forms.Padding(0); + lstObjects.Name = "lstObjects"; + lstObjects.Size = new System.Drawing.Size(194, 438); + lstObjects.TabIndex = 0; + lstObjects.SelectedIndicesChanged += lstObjects_SelectedIndicesChanged; + // + // splitter + // + splitter.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); + splitter.Location = new System.Drawing.Point(205, 5); + splitter.MinExtra = 180; + splitter.MinSize = 120; + splitter.Name = "splitter"; + splitter.Size = new System.Drawing.Size(3, 444); + splitter.TabIndex = 2; + splitter.TabStop = false; + // + // panelContent + // + panelContent.Dock = System.Windows.Forms.DockStyle.Fill; + panelContent.Location = new System.Drawing.Point(208, 5); + panelContent.Margin = new System.Windows.Forms.Padding(0); + panelContent.Name = "panelContent"; + panelContent.Size = new System.Drawing.Size(411, 444); + panelContent.TabIndex = 3; + // + // FormLuaProperties + // + AcceptButton = butOK; + AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + CancelButton = butCancel; + ClientSize = new System.Drawing.Size(624, 484); + Controls.Add(panelContent); + Controls.Add(splitter); + Controls.Add(panelLeft); + Controls.Add(panelButtons); + MaximizeBox = false; + MinimizeBox = false; + MinimumSize = new System.Drawing.Size(500, 350); + Name = "FormLuaProperties"; + Padding = new System.Windows.Forms.Padding(5); + ShowIcon = false; + ShowInTaskbar = false; + SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; + StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + Text = "Item Properties"; + panelButtons.ResumeLayout(false); + panelLeft.ResumeLayout(false); + ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.Panel panelButtons; + private DarkUI.Controls.DarkButton butReset; + private DarkUI.Controls.DarkButton butCancel; + private DarkUI.Controls.DarkButton butOK; + private System.Windows.Forms.Panel panelLeft; + private DarkUI.Controls.DarkListView lstObjects; + private System.Windows.Forms.Splitter splitter; + private System.Windows.Forms.Panel panelContent; + } +} diff --git a/WadTool/Forms/FormLuaProperties.cs b/WadTool/Forms/FormLuaProperties.cs new file mode 100644 index 0000000000..9cb1d79c02 --- /dev/null +++ b/WadTool/Forms/FormLuaProperties.cs @@ -0,0 +1,231 @@ +using DarkUI.Controls; +using DarkUI.Forms; +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using System.Windows.Forms.Integration; +using TombLib.Forms.ViewModels; +using TombLib.Forms.Views; +using TombLib.LuaProperties; +using TombLib.Wad; +using TombLib.Wad.Catalog; + +namespace WadTool +{ + public partial class FormLuaProperties : DarkForm + { + // Object management. + private readonly WadToolClass _tool; + private readonly Wad2 _wad; + private IWadObjectId _currentObjectId; + + // WPF hosting. + private readonly ElementHost _elementHost; + private readonly LuaPropertyGridControl _wpfControl; + private readonly LuaPropertyGridViewModel _viewModel; + + // Original containers for cancel/restore (key = objectId). + private readonly Dictionary _originalProperties = new Dictionary(); + + // Track which objects were actually modified. + private bool _anyChanges; + + public FormLuaProperties(WadToolClass tool, Wad2 wad, IWadObjectId initialObjectId = null) + { + _tool = tool; + _wad = wad; + _currentObjectId = initialObjectId; + + InitializeComponent(); + PopulateObjectList(); + + // Create WPF control + view model. + _viewModel = new LuaPropertyGridViewModel(); + _wpfControl = new LuaPropertyGridControl(); + _wpfControl.ViewModel = _viewModel; + + _elementHost = new ElementHost + { + Dock = DockStyle.Fill, + Child = _wpfControl + }; + + panelContent.Controls.Add(_elementHost); + + // Track actual property modifications. + _viewModel.PropertyValueChanged += (s, ev) => _anyChanges = true; + } + + #region Init + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + // Select the requested object, or the first one + if (_currentObjectId != null) + SelectObjectInList(_currentObjectId); + else if (lstObjects.Items.Count > 0) + lstObjects.SelectItem(0); + } + + #endregion + + #region Object list management + + private void PopulateObjectList() + { + lstObjects.Items.Clear(); + var gameVersion = _wad.GameVersion; + + // Add moveables + foreach (var kvp in _wad.Moveables) + { + string name = TrCatalog.GetMoveableName(gameVersion, kvp.Key.TypeId); + var item = new DarkListItem(name) + { + Tag = kvp.Key + }; + lstObjects.Items.Add(item); + } + + // Add statics + foreach (var kvp in _wad.Statics) + { + string name = TrCatalog.GetStaticName(gameVersion, kvp.Key.TypeId); + var item = new DarkListItem(name) + { + Tag = kvp.Key + }; + lstObjects.Items.Add(item); + } + } + + private void SelectObjectInList(IWadObjectId objectId) + { + for (int i = 0; i < lstObjects.Items.Count; i++) + { + if (lstObjects.Items[i].Tag is IWadObjectId id && id.Equals(objectId)) + { + lstObjects.SelectItem(i); + lstObjects.EnsureVisible(); + return; + } + } + + // Fallback: select first + if (lstObjects.Items.Count > 0) + lstObjects.SelectItem(0); + } + + private void lstObjects_SelectedIndicesChanged(object sender, EventArgs e) + { + if (lstObjects.SelectedIndices.Count == 0) + return; + + var selected = lstObjects.Items[lstObjects.SelectedIndices[0]]; + if (selected.Tag is IWadObjectId objectId) + LoadObject(objectId); + } + + #endregion + + #region Property loading + + private void LoadObject(IWadObjectId objectId) + { + var wadObject = _wad.TryGet(objectId); + if (wadObject == null) + return; + + _currentObjectId = objectId; + + // Snapshot original state for cancel/restore (only first time per object) + if (!_originalProperties.ContainsKey(objectId)) + { + var container = GetContainer(wadObject); + _originalProperties[objectId] = container?.Clone() ?? new LuaPropertyContainer(); + } + + // Determine kind and type ID + LuaPropertyObjectKind kind; + uint typeId; + string objectName; + + if (wadObject is WadMoveable) + { + kind = LuaPropertyObjectKind.Moveable; + typeId = ((WadMoveableId)objectId).TypeId; + objectName = TrCatalog.GetMoveableName(_wad.GameVersion, typeId); + } + else if (wadObject is WadStatic) + { + kind = LuaPropertyObjectKind.Static; + typeId = ((WadStaticId)objectId).TypeId; + objectName = TrCatalog.GetStaticName(_wad.GameVersion, typeId); + } + else + return; + + _viewModel.Title = objectName; + + var definitions = LuaPropertyCatalog.GetDefinitions(kind, typeId); + _viewModel.Load(definitions, GetContainer(wadObject)); + + if (definitions.Count == 0) + _viewModel.StatusMessage = "No item properties defined for this object type."; + } + + private static LuaPropertyContainer GetContainer(IWadObject wadObject) + { + if (wadObject is WadMoveable moveable) + return moveable.LuaProperties; + if (wadObject is WadStatic staticObj) + return staticObj.LuaProperties; + return null; + } + + #endregion + + #region Event handlers + + private void butOK_Click(object sender, EventArgs e) + { + // Values are already written to containers via the ViewModel's live-write. + if (_anyChanges) + _tool.ToggleUnsavedChanges(); + + DialogResult = DialogResult.OK; + Close(); + } + + private void butCancel_Click(object sender, EventArgs e) + { + // Restore all original properties + foreach (var kvp in _originalProperties) + { + var wadObject = _wad.TryGet(kvp.Key); + if (wadObject == null) + continue; + + var container = GetContainer(wadObject); + if (container != null) + { + container.Clear(); + foreach (var prop in kvp.Value.GetAll()) + container.SetValue(prop.Key, prop.Value); + } + } + + DialogResult = DialogResult.Cancel; + Close(); + } + + private void butReset_Click(object sender, EventArgs e) + { + _viewModel.ResetAll(); + } + + #endregion + } +} diff --git a/WadTool/Forms/FormLuaProperties.resx b/WadTool/Forms/FormLuaProperties.resx new file mode 100644 index 0000000000..8b2ff64a11 --- /dev/null +++ b/WadTool/Forms/FormLuaProperties.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/WadTool/Forms/FormMain.Designer.cs b/WadTool/Forms/FormMain.Designer.cs index 1531bf13cf..327101d647 100644 --- a/WadTool/Forms/FormMain.Designer.cs +++ b/WadTool/Forms/FormMain.Designer.cs @@ -46,6 +46,7 @@ private void InitializeComponent() optionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); animatedTexturesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); meshEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + itemPropertiesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator(); convertDestinationWadToTombEngineToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator(); @@ -103,6 +104,7 @@ private void InitializeComponent() editAnimationsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); editSkeletonToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); editMeshToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + editPropertiesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem6 = new System.Windows.Forms.ToolStripSeparator(); toolStripMenuItemMoveablesChangeSlot = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItemMoveablesDelete = new System.Windows.Forms.ToolStripMenuItem(); @@ -112,6 +114,7 @@ private void InitializeComponent() toolStripMenuItem4 = new System.Windows.Forms.ToolStripSeparator(); changeSlorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); deleteObjectToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + editPropertiesToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); statusStrip.SuspendLayout(); darkMenuStrip1.SuspendLayout(); darkToolStrip1.SuspendLayout(); @@ -176,7 +179,7 @@ private void InitializeComponent() newWad2ToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); newWad2ToolStripMenuItem.Name = "newWad2ToolStripMenuItem"; newWad2ToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N; - newWad2ToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + newWad2ToolStripMenuItem.Size = new System.Drawing.Size(328, 22); newWad2ToolStripMenuItem.Text = "New Wad2"; newWad2ToolStripMenuItem.Click += newWad2ToolStripMenuItem_Click; // @@ -185,7 +188,7 @@ private void InitializeComponent() toolStripMenuItem3.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); toolStripMenuItem3.Margin = new System.Windows.Forms.Padding(0, 0, 0, 1); toolStripMenuItem3.Name = "toolStripMenuItem3"; - toolStripMenuItem3.Size = new System.Drawing.Size(340, 6); + toolStripMenuItem3.Size = new System.Drawing.Size(325, 6); // // openSourceWADToolStripMenuItem // @@ -194,7 +197,7 @@ private void InitializeComponent() openSourceWADToolStripMenuItem.Image = Properties.Resources.general_Open_16; openSourceWADToolStripMenuItem.Name = "openSourceWADToolStripMenuItem"; openSourceWADToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O; - openSourceWADToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + openSourceWADToolStripMenuItem.Size = new System.Drawing.Size(328, 22); openSourceWADToolStripMenuItem.Text = "Open source"; openSourceWADToolStripMenuItem.Click += openSourceWADToolStripMenuItem_Click; // @@ -205,7 +208,7 @@ private void InitializeComponent() openDestinationWadToolStripMenuItem.Image = Properties.Resources.opened_folder_16; openDestinationWadToolStripMenuItem.Name = "openDestinationWadToolStripMenuItem"; openDestinationWadToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Shift | System.Windows.Forms.Keys.O; - openDestinationWadToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + openDestinationWadToolStripMenuItem.Size = new System.Drawing.Size(328, 22); openDestinationWadToolStripMenuItem.Text = "Open destination"; openDestinationWadToolStripMenuItem.Click += openDestinationWad2ToolStripMenuItem_Click; // @@ -214,7 +217,7 @@ private void InitializeComponent() openRecentToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); openRecentToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); openRecentToolStripMenuItem.Name = "openRecentToolStripMenuItem"; - openRecentToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + openRecentToolStripMenuItem.Size = new System.Drawing.Size(328, 22); openRecentToolStripMenuItem.Text = "Open recent..."; // // openReferenceLevelToolStripMenuItem @@ -223,7 +226,7 @@ private void InitializeComponent() openReferenceLevelToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); openReferenceLevelToolStripMenuItem.Name = "openReferenceLevelToolStripMenuItem"; openReferenceLevelToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.O; - openReferenceLevelToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + openReferenceLevelToolStripMenuItem.Size = new System.Drawing.Size(328, 22); openReferenceLevelToolStripMenuItem.Text = "Open reference Tomb Editor project"; openReferenceLevelToolStripMenuItem.Click += openReferenceLevelToolStripMenuItem_Click; // @@ -233,7 +236,7 @@ private void InitializeComponent() closeReferenceLevelToolStripMenuItem.Enabled = false; closeReferenceLevelToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(153, 153, 153); closeReferenceLevelToolStripMenuItem.Name = "closeReferenceLevelToolStripMenuItem"; - closeReferenceLevelToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + closeReferenceLevelToolStripMenuItem.Size = new System.Drawing.Size(328, 22); closeReferenceLevelToolStripMenuItem.Text = "Close reference Tomb Editor project"; closeReferenceLevelToolStripMenuItem.Click += closeReferenceLevelToolStripMenuItem_Click; // @@ -242,7 +245,7 @@ private void InitializeComponent() toolStripMenuItem1.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); toolStripMenuItem1.Margin = new System.Windows.Forms.Padding(0, 0, 0, 1); toolStripMenuItem1.Name = "toolStripMenuItem1"; - toolStripMenuItem1.Size = new System.Drawing.Size(340, 6); + toolStripMenuItem1.Size = new System.Drawing.Size(325, 6); // // saveWad2ToolStripMenuItem // @@ -252,7 +255,7 @@ private void InitializeComponent() saveWad2ToolStripMenuItem.Image = Properties.Resources.save_16; saveWad2ToolStripMenuItem.Name = "saveWad2ToolStripMenuItem"; saveWad2ToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S; - saveWad2ToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + saveWad2ToolStripMenuItem.Size = new System.Drawing.Size(328, 22); saveWad2ToolStripMenuItem.Text = "Save Wad2"; saveWad2ToolStripMenuItem.Click += butSave_Click; // @@ -264,7 +267,7 @@ private void InitializeComponent() saveWad2AsToolStripMenuItem.Image = Properties.Resources.save_as_16; saveWad2AsToolStripMenuItem.Name = "saveWad2AsToolStripMenuItem"; saveWad2AsToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Shift | System.Windows.Forms.Keys.S; - saveWad2AsToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + saveWad2AsToolStripMenuItem.Size = new System.Drawing.Size(328, 22); saveWad2AsToolStripMenuItem.Text = "Save Wad2 as..."; saveWad2AsToolStripMenuItem.Click += saveWad2AsToolStripMenuItem_Click; // @@ -273,7 +276,7 @@ private void InitializeComponent() toolStripMenuItem2.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); toolStripMenuItem2.Margin = new System.Windows.Forms.Padding(0, 0, 0, 1); toolStripMenuItem2.Name = "toolStripMenuItem2"; - toolStripMenuItem2.Size = new System.Drawing.Size(340, 6); + toolStripMenuItem2.Size = new System.Drawing.Size(325, 6); // // exitToolStripMenuItem // @@ -282,7 +285,7 @@ private void InitializeComponent() exitToolStripMenuItem.Image = Properties.Resources.door_opened_16; exitToolStripMenuItem.Name = "exitToolStripMenuItem"; exitToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.F4; - exitToolStripMenuItem.Size = new System.Drawing.Size(343, 22); + exitToolStripMenuItem.Size = new System.Drawing.Size(328, 22); exitToolStripMenuItem.Text = "Exit"; exitToolStripMenuItem.Click += exitToolStripMenuItem_Click; // @@ -388,10 +391,10 @@ private void InitializeComponent() // optionsToolStripMenuItem // optionsToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); - optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { animatedTexturesToolStripMenuItem, meshEditorToolStripMenuItem, toolStripSeparator4, convertDestinationWadToTombEngineToolStripMenuItem, toolStripSeparator2, optionsToolStripMenuItem1 }); + optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { animatedTexturesToolStripMenuItem, meshEditorToolStripMenuItem, itemPropertiesToolStripMenuItem, toolStripSeparator4, convertDestinationWadToTombEngineToolStripMenuItem, toolStripSeparator2, optionsToolStripMenuItem1 }); optionsToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); optionsToolStripMenuItem.Name = "optionsToolStripMenuItem"; - optionsToolStripMenuItem.Size = new System.Drawing.Size(47, 20); + optionsToolStripMenuItem.Size = new System.Drawing.Size(46, 20); optionsToolStripMenuItem.Text = "Tools"; // // animatedTexturesToolStripMenuItem @@ -399,7 +402,7 @@ private void InitializeComponent() animatedTexturesToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); animatedTexturesToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); animatedTexturesToolStripMenuItem.Name = "animatedTexturesToolStripMenuItem"; - animatedTexturesToolStripMenuItem.Size = new System.Drawing.Size(234, 22); + animatedTexturesToolStripMenuItem.Size = new System.Drawing.Size(233, 22); animatedTexturesToolStripMenuItem.Text = "Animated textures"; animatedTexturesToolStripMenuItem.Click += animatedTexturesToolStripMenuItem_Click; // @@ -408,24 +411,33 @@ private void InitializeComponent() meshEditorToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); meshEditorToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); meshEditorToolStripMenuItem.Name = "meshEditorToolStripMenuItem"; - meshEditorToolStripMenuItem.Size = new System.Drawing.Size(234, 22); + meshEditorToolStripMenuItem.Size = new System.Drawing.Size(233, 22); meshEditorToolStripMenuItem.Text = "Mesh editor"; meshEditorToolStripMenuItem.Click += meshEditorToolStripMenuItem_Click; // + // itemPropertiesToolStripMenuItem + // + itemPropertiesToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); + itemPropertiesToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + itemPropertiesToolStripMenuItem.Name = "itemPropertiesToolStripMenuItem"; + itemPropertiesToolStripMenuItem.Size = new System.Drawing.Size(233, 22); + itemPropertiesToolStripMenuItem.Text = "Property editor"; + itemPropertiesToolStripMenuItem.Click += itemPropertiesToolStripMenuItem_Click; + // // toolStripSeparator4 // toolStripSeparator4.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); toolStripSeparator4.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); toolStripSeparator4.Margin = new System.Windows.Forms.Padding(0, 0, 0, 1); toolStripSeparator4.Name = "toolStripSeparator4"; - toolStripSeparator4.Size = new System.Drawing.Size(231, 6); + toolStripSeparator4.Size = new System.Drawing.Size(230, 6); // // convertDestinationWadToTombEngineToolStripMenuItem // convertDestinationWadToTombEngineToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); convertDestinationWadToTombEngineToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); convertDestinationWadToTombEngineToolStripMenuItem.Name = "convertDestinationWadToTombEngineToolStripMenuItem"; - convertDestinationWadToTombEngineToolStripMenuItem.Size = new System.Drawing.Size(234, 22); + convertDestinationWadToTombEngineToolStripMenuItem.Size = new System.Drawing.Size(233, 22); convertDestinationWadToTombEngineToolStripMenuItem.Text = "Convert wad to TombEngine..."; convertDestinationWadToTombEngineToolStripMenuItem.Click += convertDestinationToTombEngineToolStripMenuItem_Click; // @@ -435,14 +447,14 @@ private void InitializeComponent() toolStripSeparator2.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); toolStripSeparator2.Margin = new System.Windows.Forms.Padding(0, 0, 0, 1); toolStripSeparator2.Name = "toolStripSeparator2"; - toolStripSeparator2.Size = new System.Drawing.Size(231, 6); + toolStripSeparator2.Size = new System.Drawing.Size(230, 6); // // optionsToolStripMenuItem1 // optionsToolStripMenuItem1.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); optionsToolStripMenuItem1.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); optionsToolStripMenuItem1.Name = "optionsToolStripMenuItem1"; - optionsToolStripMenuItem1.Size = new System.Drawing.Size(234, 22); + optionsToolStripMenuItem1.Size = new System.Drawing.Size(233, 22); optionsToolStripMenuItem1.Text = "Options..."; optionsToolStripMenuItem1.Click += optionsToolStripMenuItem_Click; // @@ -460,7 +472,7 @@ private void InitializeComponent() aboutWadToolToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); aboutWadToolToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); aboutWadToolToolStripMenuItem.Name = "aboutWadToolToolStripMenuItem"; - aboutWadToolToolStripMenuItem.Size = new System.Drawing.Size(169, 22); + aboutWadToolToolStripMenuItem.Size = new System.Drawing.Size(168, 22); aboutWadToolToolStripMenuItem.Text = "About Wad Tool..."; aboutWadToolToolStripMenuItem.Click += aboutWadToolToolStripMenuItem_Click; // @@ -990,9 +1002,9 @@ private void InitializeComponent() // contextMenuMoveableItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); contextMenuMoveableItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); - contextMenuMoveableItem.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { editAnimationsToolStripMenuItem, editSkeletonToolStripMenuItem, editMeshToolStripMenuItem, toolStripMenuItem6, toolStripMenuItemMoveablesChangeSlot, toolStripMenuItemMoveablesDelete }); + contextMenuMoveableItem.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { editAnimationsToolStripMenuItem, editSkeletonToolStripMenuItem, editMeshToolStripMenuItem, editPropertiesToolStripMenuItem, toolStripMenuItem6, toolStripMenuItemMoveablesChangeSlot, toolStripMenuItemMoveablesDelete }); contextMenuMoveableItem.Name = "contextMenuMoveableItem"; - contextMenuMoveableItem.Size = new System.Drawing.Size(166, 121); + contextMenuMoveableItem.Size = new System.Drawing.Size(166, 143); // // editAnimationsToolStripMenuItem // @@ -1023,6 +1035,15 @@ private void InitializeComponent() editMeshToolStripMenuItem.Text = "Edit meshes..."; editMeshToolStripMenuItem.Click += meshEditorToolStripMenuItem_Click; // + // editPropertiesToolStripMenuItem + // + editPropertiesToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); + editPropertiesToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + editPropertiesToolStripMenuItem.Name = "editPropertiesToolStripMenuItem"; + editPropertiesToolStripMenuItem.Size = new System.Drawing.Size(165, 22); + editPropertiesToolStripMenuItem.Text = "Edit properties..."; + editPropertiesToolStripMenuItem.Click += editPropertiesToolStripMenuItem_Click; + // // toolStripMenuItem6 // toolStripMenuItem6.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); @@ -1055,9 +1076,9 @@ private void InitializeComponent() // cmStatics.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); cmStatics.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); - cmStatics.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { editObjectToolStripMenuItem, editMeshToolStripMenuItem1, toolStripMenuItem4, changeSlorToolStripMenuItem, deleteObjectToolStripMenuItem }); + cmStatics.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { editObjectToolStripMenuItem, editMeshToolStripMenuItem1, editPropertiesToolStripMenuItem1, toolStripMenuItem4, changeSlorToolStripMenuItem, deleteObjectToolStripMenuItem }); cmStatics.Name = "cmObject"; - cmStatics.Size = new System.Drawing.Size(147, 99); + cmStatics.Size = new System.Drawing.Size(181, 143); // // editObjectToolStripMenuItem // @@ -1065,7 +1086,7 @@ private void InitializeComponent() editObjectToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); editObjectToolStripMenuItem.Image = Properties.Resources.edit_16; editObjectToolStripMenuItem.Name = "editObjectToolStripMenuItem"; - editObjectToolStripMenuItem.Size = new System.Drawing.Size(146, 22); + editObjectToolStripMenuItem.Size = new System.Drawing.Size(180, 22); editObjectToolStripMenuItem.Text = "Edit object..."; editObjectToolStripMenuItem.Click += editObjectToolStripMenuItem_Click; // @@ -1074,7 +1095,7 @@ private void InitializeComponent() editMeshToolStripMenuItem1.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); editMeshToolStripMenuItem1.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); editMeshToolStripMenuItem1.Name = "editMeshToolStripMenuItem1"; - editMeshToolStripMenuItem1.Size = new System.Drawing.Size(146, 22); + editMeshToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); editMeshToolStripMenuItem1.Text = "Edit mesh..."; editMeshToolStripMenuItem1.Click += meshEditorToolStripMenuItem_Click; // @@ -1084,7 +1105,7 @@ private void InitializeComponent() toolStripMenuItem4.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); toolStripMenuItem4.Margin = new System.Windows.Forms.Padding(0, 0, 0, 1); toolStripMenuItem4.Name = "toolStripMenuItem4"; - toolStripMenuItem4.Size = new System.Drawing.Size(143, 6); + toolStripMenuItem4.Size = new System.Drawing.Size(177, 6); // // changeSlorToolStripMenuItem // @@ -1092,7 +1113,7 @@ private void InitializeComponent() changeSlorToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); changeSlorToolStripMenuItem.Image = Properties.Resources.replace_16; changeSlorToolStripMenuItem.Name = "changeSlorToolStripMenuItem"; - changeSlorToolStripMenuItem.Size = new System.Drawing.Size(146, 22); + changeSlorToolStripMenuItem.Size = new System.Drawing.Size(180, 22); changeSlorToolStripMenuItem.Text = "Change slot..."; changeSlorToolStripMenuItem.Click += changeSlotToolStripMenuItem_Click; // @@ -1102,10 +1123,19 @@ private void InitializeComponent() deleteObjectToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); deleteObjectToolStripMenuItem.Image = Properties.Resources.trash_16; deleteObjectToolStripMenuItem.Name = "deleteObjectToolStripMenuItem"; - deleteObjectToolStripMenuItem.Size = new System.Drawing.Size(146, 22); + deleteObjectToolStripMenuItem.Size = new System.Drawing.Size(180, 22); deleteObjectToolStripMenuItem.Text = "Delete object"; deleteObjectToolStripMenuItem.Click += deleteObjectToolStripMenuItem_Click; // + // editPropertiesToolStripMenuItem1 + // + editPropertiesToolStripMenuItem1.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); + editPropertiesToolStripMenuItem1.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + editPropertiesToolStripMenuItem1.Name = "editPropertiesToolStripMenuItem1"; + editPropertiesToolStripMenuItem1.Size = new System.Drawing.Size(180, 22); + editPropertiesToolStripMenuItem1.Text = "Edit properties..."; + editPropertiesToolStripMenuItem1.Click += editPropertiesToolStripMenuItem_Click; + // // FormMain // AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -1243,6 +1273,9 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem convertToTiledToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem animatedTexturesToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator4; + private System.Windows.Forms.ToolStripMenuItem itemPropertiesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem editPropertiesToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem editPropertiesToolStripMenuItem1; } } diff --git a/WadTool/Forms/FormMain.cs b/WadTool/Forms/FormMain.cs index 59aedd990d..59e789dfed 100644 --- a/WadTool/Forms/FormMain.cs +++ b/WadTool/Forms/FormMain.cs @@ -87,25 +87,24 @@ private void Tool_EditorEventRaised(IEditorEvent obj) panel3D.Invalidate(); - if (_tool.DestinationWad != null) + bool isWadLoaded = _tool.DestinationWad != null; + if (isWadLoaded) { labelStatistics.Text = "Moveables: " + _tool.DestinationWad.Moveables.Count + " | " + "Statics: " + _tool.DestinationWad.Statics.Count + " | " + "Sprite sequences: " + _tool.DestinationWad.SpriteSequences.Count + " | " + "Textures: " + _tool.DestinationWad.MeshTexturesUnique.Count + " | " + "Texture infos: " + _tool.DestinationWad.MeshTexInfosUnique.Count; - - meshEditorToolStripMenuItem.Enabled = true; - animatedTexturesToolStripMenuItem.Enabled = true; - convertDestinationWadToTombEngineToolStripMenuItem.Enabled = true; } else { labelStatistics.Text = ""; - meshEditorToolStripMenuItem.Enabled = false; - animatedTexturesToolStripMenuItem.Enabled = false; - convertDestinationWadToTombEngineToolStripMenuItem.Enabled = false; } + + meshEditorToolStripMenuItem.Enabled = + animatedTexturesToolStripMenuItem.Enabled = + convertDestinationWadToTombEngineToolStripMenuItem.Enabled = + itemPropertiesToolStripMenuItem.Enabled = isWadLoaded; } if (obj is WadToolClass.SourceWadChangedEvent || obj is InitEvent) @@ -551,6 +550,11 @@ private void butEditSpriteSequence_Click(object sender, EventArgs e) WadActions.EditObject(_tool, this, DeviceManager.DefaultDeviceManager); } + private void itemPropertiesToolStripMenuItem_Click(object sender, EventArgs e) + { + WadActions.EditLuaProperties(_tool, this, null); + } + private void changeSlotToolStripMenuItem_Click(object sender, EventArgs e) { butChangeSlot_Click(null, null); @@ -708,5 +712,10 @@ private void animatedTexturesToolStripMenuItem_Click(object sender, EventArgs e) form.ShowDialog(); } } + + private void editPropertiesToolStripMenuItem_Click(object sender, EventArgs e) + { + WadActions.EditLuaProperties(_tool, this, _tool.DestinationWad?.TryGet(_tool.MainSelection?.Id).Id); + } } } diff --git a/WadTool/WadActions.cs b/WadTool/WadActions.cs index afedd1a567..7457aef8aa 100644 --- a/WadTool/WadActions.cs +++ b/WadTool/WadActions.cs @@ -965,6 +965,18 @@ public static void EditObject(WadToolClass tool, IWin32Window owner, DeviceManag } } + public static void EditLuaProperties(WadToolClass tool, IWin32Window owner, IWadObjectId focusObjectId = null) + { + if (tool.DestinationWad == null) + { + tool.SendMessage("No destination wad is loaded.", PopupType.Info); + return; + } + + using (var form = new FormLuaProperties(tool, tool.DestinationWad, focusObjectId)) + form.ShowDialog(owner); + } + public static void DeleteObjects(WadToolClass tool, IWin32Window owner, WadArea wadArea, List ObjectIdsToDelete) { if (ObjectIdsToDelete.Count == 0) diff --git a/WadTool/WadTool.csproj b/WadTool/WadTool.csproj index aa3f17900c..66464366f6 100644 --- a/WadTool/WadTool.csproj +++ b/WadTool/WadTool.csproj @@ -4,6 +4,7 @@ WinExe false true + true true Debug;Release x64;x86