From bda1a7b0f5fa0e8c0043bfc97d98e721cf111cfe Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:39:32 +0100 Subject: [PATCH 01/30] First iteration (no tests) --- TombEditor/Command.cs | 1 + TombEditor/Forms/FormMain.Designer.cs | 13 +- TombEditor/Forms/FormMain.cs | 4 +- .../ToolWindows/LuaProperties.Designer.cs | 30 ++ TombEditor/ToolWindows/LuaProperties.cs | 139 +++++ .../ViewModels/LuaPropertyGridViewModel.cs | 183 +++++++ .../ViewModels/LuaPropertyRowViewModel.cs | 282 ++++++++++ .../LuaPropertyEditorTemplateSelector.cs | 48 ++ .../Views/LuaPropertyGridControl.xaml | 251 +++++++++ .../Views/LuaPropertyGridControl.xaml.cs | 102 ++++ .../TEN Property Catalogs/Example.xml | 72 +++ .../Catalogs/TEN Property Catalogs/Lara.xml | 217 ++++++++ .../TombEngine/LevelCompilerTombEngine.cs | 80 +++ .../Compilers/TombEngine/TombEngine.cs | 3 + TombLib/TombLib/LevelData/IO/Prj2Chunks.cs | 2 + TombLib/TombLib/LevelData/IO/Prj2Loader.cs | 55 ++ TombLib/TombLib/LevelData/IO/Prj2Writer.cs | 29 +- .../LevelData/Instances/MoveableInstance.cs | 7 + .../LevelData/Instances/StaticInstance.cs | 7 + .../LuaProperties/LuaPropertyCatalog.cs | 286 ++++++++++ .../LuaProperties/LuaPropertyContainer.cs | 116 ++++ .../LuaProperties/LuaPropertyDefinition.cs | 59 ++ .../LuaProperties/LuaPropertyScriptBuilder.cs | 181 +++++++ .../TombLib/LuaProperties/LuaPropertyType.cs | 21 + .../TombLib/LuaProperties/LuaValueParser.cs | 510 ++++++++++++++++++ TombLib/TombLib/TombLib.csproj | 3 + TombLib/TombLib/Utils/ScriptingUtils.cs | 1 + TombLib/TombLib/Wad/Wad2Chunks.cs | 5 + TombLib/TombLib/Wad/Wad2Loader.cs | 52 ++ TombLib/TombLib/Wad/Wad2Writer.cs | 28 + TombLib/TombLib/Wad/WadMoveable.cs | 8 + TombLib/TombLib/Wad/WadStatic.cs | 8 + WadTool/Forms/FormLuaProperties.cs | 224 ++++++++ WadTool/Forms/FormMain.Designer.cs | 15 +- WadTool/Forms/FormMain.cs | 8 + WadTool/WadActions.cs | 20 + WadTool/WadTool.csproj | 1 + 37 files changed, 3066 insertions(+), 5 deletions(-) create mode 100644 TombEditor/ToolWindows/LuaProperties.Designer.cs create mode 100644 TombEditor/ToolWindows/LuaProperties.cs create mode 100644 TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs create mode 100644 TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs create mode 100644 TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs create mode 100644 TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml create mode 100644 TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs create mode 100644 TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml create mode 100644 TombLib/TombLib/Catalogs/TEN Property Catalogs/Lara.xml create mode 100644 TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs create mode 100644 TombLib/TombLib/LuaProperties/LuaPropertyContainer.cs create mode 100644 TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs create mode 100644 TombLib/TombLib/LuaProperties/LuaPropertyScriptBuilder.cs create mode 100644 TombLib/TombLib/LuaProperties/LuaPropertyType.cs create mode 100644 TombLib/TombLib/LuaProperties/LuaValueParser.cs create mode 100644 WadTool/Forms/FormLuaProperties.cs diff --git a/TombEditor/Command.cs b/TombEditor/Command.cs index 1999075f17..66f1d97508 100644 --- a/TombEditor/Command.cs +++ b/TombEditor/Command.cs @@ -1685,6 +1685,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("ShowLuaProperties", "Show Lua properties", CommandType.Windows, (CommandArgs args) => args.Editor.ToggleToolWindow(typeof(LuaProperties))); AddCommand("ShowStatistics", "Statistics display", CommandType.Windows, delegate (CommandArgs args) { diff --git a/TombEditor/Forms/FormMain.Designer.cs b/TombEditor/Forms/FormMain.Designer.cs index 21678c7455..b529f625d0 100644 --- a/TombEditor/Forms/FormMain.Designer.cs +++ b/TombEditor/Forms/FormMain.Designer.cs @@ -202,6 +202,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(); @@ -1866,7 +1867,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, triggerListToolStripMenuItem, lightingToolStripMenuItem, paletteToolStripMenuItem, texturePanelToolStripMenuItem, objectListToolStripMenuItem, statisticsToolStripMenuItem, dockableToolStripMenuItem, floatingToolStripMenuItem }); + windowToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { restoreDefaultLayoutToolStripMenuItem, toolStripMenuSeparator14, sectorOptionsToolStripMenuItem, roomOptionsToolStripMenuItem, itemBrowserToolStripMenuItem, importedGeometryBrowserToolstripMenuItem, 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); @@ -1970,6 +1971,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 = "ShowLuaProperties"; + luaPropertiesToolStripMenuItem.Text = "ShowLuaProperties"; + // // statisticsToolStripMenuItem // statisticsToolStripMenuItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); @@ -2434,6 +2444,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 e1b2a591e0..f4ad8908a1 100644 --- a/TombEditor/Forms/FormMain.cs +++ b/TombEditor/Forms/FormMain.cs @@ -36,7 +36,8 @@ public partial class FormMain : DarkForm new Palette(), new TexturePanel(), new ObjectList(), - new ToolPalette() + new ToolPalette(), + new LuaProperties() }; // Floating tool boxes are placed on 3D view at runtime @@ -564,6 +565,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/LuaProperties.Designer.cs b/TombEditor/ToolWindows/LuaProperties.Designer.cs new file mode 100644 index 0000000000..d905ffa889 --- /dev/null +++ b/TombEditor/ToolWindows/LuaProperties.Designer.cs @@ -0,0 +1,30 @@ +namespace TombEditor.ToolWindows +{ + partial class LuaProperties + { + /// + /// 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(); + // + // LuaProperties + // + this.DockText = "Lua Properties"; + this.Name = "LuaProperties"; + this.Size = new System.Drawing.Size(300, 400); + this.ResumeLayout(false); + } + + #endregion + } +} diff --git a/TombEditor/ToolWindows/LuaProperties.cs b/TombEditor/ToolWindows/LuaProperties.cs new file mode 100644 index 0000000000..a584ec1256 --- /dev/null +++ b/TombEditor/ToolWindows/LuaProperties.cs @@ -0,0 +1,139 @@ +using DarkUI.Docking; +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.LevelData; +using TombLib.LuaProperties; +using TombLib.Wad; + +namespace TombEditor.ToolWindows +{ + public partial class LuaProperties : 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 LuaProperties() + { + 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 oce) + { + if (oce.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) + { + UpdatePropertyGrid(); + } + } + + private void UpdatePropertyGrid() + { + var selected = _editor.SelectedObject; + + // Only show for TombEngine levels + if (!_editor.Level.IsTombEngine) + { + _viewModel.Clear(); + _viewModel.Title = "Lua Properties"; + _currentObject = null; + return; + } + + if (selected is MoveableInstance moveable) + { + _currentObject = moveable; + var typeId = moveable.WadObjectId.TypeId; + var definitions = LuaPropertyCatalog.GetDefinitions(LuaPropertyObjectKind.Moveable, typeId); + + _viewModel.Title = $"Properties: {moveable.ItemType.ToString()}"; + _viewModel.Load(definitions, moveable.LuaProperties); + } + else if (selected is StaticInstance staticObj) + { + _currentObject = staticObj; + var typeId = staticObj.WadObjectId.TypeId; + var definitions = LuaPropertyCatalog.GetDefinitions(LuaPropertyObjectKind.Static, typeId); + + _viewModel.Title = $"Properties: {staticObj.ItemType.ToString()}"; + _viewModel.Load(definitions, staticObj.LuaProperties); + } + else + { + _currentObject = null; + _viewModel.Clear(); + _viewModel.Title = "Lua Properties"; + } + } + + /// + /// When a property value changes in the WPF grid, notify the editor + /// that the object has been modified so undo/save state is updated. + /// + private void OnPropertyValueChanged(object sender, EventArgs e) + { + if (_currentObject != null) + { + _editor.ObjectChange(_currentObject, ObjectChangeType.Change); + } + } + } +} diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs new file mode 100644 index 0000000000..a2ddb34e2d --- /dev/null +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs @@ -0,0 +1,183 @@ +// 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.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +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 class LuaPropertyGridViewModel : INotifyPropertyChanged + { + /// + /// 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; + + /// + /// The display title for the property grid header. + /// + public string Title + { + get => _title; + set { _title = value; OnPropertyChanged(); } + } + private string _title = "Properties"; + + /// + /// Fired when any property value changes. The sender is the modified row ViewModel. + /// + public event EventHandler PropertyValueChanged; + + /// + /// 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. + public void Load(List definitions, LuaPropertyContainer container) + { + 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; + + // Get the current value from container, falling back to definition default. + string currentValue = container?.GetValue(definition.InternalName) + ?? definition.DefaultValue + ?? LuaValueParser.GetDefaultBoxedValue(definition.Type); + + var row = new LuaPropertyRowViewModel(definition, currentValue); + 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. + /// + 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); + } + + #region INotifyPropertyChanged + + public event PropertyChangedEventHandler PropertyChanged; + private void OnPropertyChanged([CallerMemberName] string name = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + #endregion + } +} diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs new file mode 100644 index 0000000000..6892a99d95 --- /dev/null +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs @@ -0,0 +1,282 @@ +// ViewModel for a single property row in the Lua property grid. +// Manages the binding between a LuaPropertyDefinition and its current boxed value. + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.CompilerServices; +using TombLib.LuaProperties; + +namespace TombLib.Forms.ViewModels +{ + /// + /// ViewModel for a single row in the Lua property grid. + /// Wraps a with an editable current value. + /// + public class LuaPropertyRowViewModel : INotifyPropertyChanged + { + private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; + + /// + /// 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.InternalName + : Definition.Description; + + /// + /// Category grouping label. Empty string means uncategorized. + /// + public string Category => Definition.Category ?? string.Empty; + + /// + /// 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) + + // --- Bool --- + public bool BoolValue + { + get => LuaValueParser.UnboxBool(_boxedValue); + set => BoxedValue = LuaValueParser.BoxBool(value); + } + + // --- Int --- + public int IntValue + { + get => LuaValueParser.UnboxInt(_boxedValue); + set => BoxedValue = LuaValueParser.BoxInt(value); + } + + // --- Float --- + public float FloatValue + { + get => LuaValueParser.UnboxFloat(_boxedValue); + set => BoxedValue = LuaValueParser.BoxFloat(value); + } + + // --- String --- + public string StringValue + { + get => LuaValueParser.UnboxString(_boxedValue); + set => BoxedValue = LuaValueParser.BoxString(value); + } + + // --- Vec2 --- + 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); } + } + + // --- Vec3 --- + 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); } + } + + // --- Rotation --- + 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); } + } + + // --- Color --- + public byte ColorR + { + get => LuaValueParser.UnboxColor(_boxedValue)[0]; + set { var c = LuaValueParser.UnboxColor(_boxedValue); BoxedValue = LuaValueParser.BoxColor(value, c[1], c[2], c[3]); } + } + public byte ColorG + { + get => LuaValueParser.UnboxColor(_boxedValue)[1]; + set { var c = LuaValueParser.UnboxColor(_boxedValue); BoxedValue = LuaValueParser.BoxColor(c[0], value, c[2], c[3]); } + } + public byte ColorB + { + get => LuaValueParser.UnboxColor(_boxedValue)[2]; + set { var c = LuaValueParser.UnboxColor(_boxedValue); BoxedValue = LuaValueParser.BoxColor(c[0], c[1], value, c[3]); } + } + 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]); + } + } + + // --- Time --- + 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); } + } + + #endregion + + public LuaPropertyRowViewModel(LuaPropertyDefinition definition, string initialBoxedValue = null) + { + Definition = definition ?? throw new ArgumentNullException(nameof(definition)); + _boxedValue = initialBoxedValue ?? definition.DefaultValue ?? LuaValueParser.GetDefaultBoxedValue(definition.Type); + } + + /// + /// Resets the value to the definition's default. + /// + public void ResetToDefault() + { + BoxedValue = Definition.DefaultValue ?? LuaValueParser.GetDefaultBoxedValue(Definition.Type); + } + + /// + /// Returns true if the current value differs from the default. + /// + public bool IsModified => + !string.Equals(_boxedValue, Definition.DefaultValue, StringComparison.Ordinal); + + #region INotifyPropertyChanged + + public event PropertyChangedEventHandler PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string name = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + /// + /// 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; + } + } + + #endregion + } +} diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs b/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs new file mode 100644 index 0000000000..acb66e8cea --- /dev/null +++ b/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs @@ -0,0 +1,48 @@ +// 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. + +using System.Windows; +using System.Windows.Controls; +using TombLib.LuaProperties; + +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 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; + } + } + + 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..2b1530fe48 --- /dev/null +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs new file mode 100644 index 0000000000..f9b8160a52 --- /dev/null +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs @@ -0,0 +1,102 @@ +// Code-behind for LuaPropertyGridControl. +// Handles color picker interaction (opens RealtimeColorDialog via WinForms interop) +// and provides value converters used in the XAML. + +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using DarkUI.WPF.CustomControls; +using TombLib.Controls; +using TombLib.Forms.ViewModels; + +namespace TombLib.Forms.Views +{ + /// + /// WPF property grid control for editing TombEngine Lua properties. + /// Hostable inside WinForms DarkForm/DarkToolWindow via ElementHost. + /// Uses DarkUI.WPF styled controls (NumericUpDown, ColorPickerButton, etc.). + /// + 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(); + } + + /// + /// Gets or sets the ViewModel. Sets DataContext. + /// + public LuaPropertyGridViewModel ViewModel + { + get => DataContext as LuaPropertyGridViewModel; + set => DataContext = value; + } + + /// + /// Handles the color picker click on the ColorPickerButton. + /// Opens the RealtimeColorDialog (WinForms) and updates the property 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; + } + } + } + } + + /// + /// Converter: collapses element when string is null or empty. + /// Used for category headers — hides header when category is empty. + /// + internal class StringEmptyToCollapsedConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var str = value as string; + 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..755714c0f7 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/TombLib/TombLib/Catalogs/TEN Property Catalogs/Lara.xml b/TombLib/TombLib/Catalogs/TEN Property Catalogs/Lara.xml new file mode 100644 index 0000000000..0c36cda612 --- /dev/null +++ b/TombLib/TombLib/Catalogs/TEN Property Catalogs/Lara.xml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs index 3da0cda49d..d5ace986a9 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; @@ -142,6 +143,7 @@ public override CompilerStatistics CompileLevel(CancellationToken cancelToken) _progressReporter.ReportInfo("\nWriting level file...\n"); + BuildLuaPropertyScript(); WriteLevelTombEngine(); cancelToken.ThrowIfCancellationRequested(); @@ -543,6 +545,84 @@ private void CopyNodeScripts() }); } + /// + /// Collects all Level 1 (global/wad) and Level 2 (per-instance) Lua properties + /// and writes them to a Lua script file alongside the compiled level. + /// The script is placed in Scripts/Engine/ and named after the level file. + /// + private string _luaPropertyScript = string.Empty; + + private void BuildLuaPropertyScript() + { + var gameVersion = _level.Settings.GameVersion; + + // Collect Level 1 properties: global per-object-type from wad2 files + var globalMoveableProps = new Dictionary(); + var globalStaticProps = new Dictionary(); + + foreach (var wadRef in _level.Settings.Wads) + { + if (wadRef.Wad == null) + continue; + + foreach (var mov in wadRef.Wad.Moveables) + { + if (mov.Value.LuaProperties == null || !mov.Value.LuaProperties.HasProperties) + continue; + + string slotName = TrCatalog.GetMoveableName(gameVersion, mov.Key.TypeId); + if (!string.IsNullOrEmpty(slotName)) + globalMoveableProps[slotName] = mov.Value.LuaProperties; + } + + foreach (var stat in wadRef.Wad.Statics) + { + if (stat.Value.LuaProperties == null || !stat.Value.LuaProperties.HasProperties) + continue; + + globalStaticProps[stat.Key.TypeId] = stat.Value.LuaProperties; + } + } + + // Collect Level 2 properties: per-instance from rooms + var instanceMoveableProps = new Dictionary(); + var instanceStaticProps = new Dictionary(); + + foreach (var room in _level.ExistingRooms) + { + foreach (var obj in room.Objects) + { + if (obj is MoveableInstance mov && mov.LuaProperties != null && mov.LuaProperties.HasProperties) + { + if (!string.IsNullOrEmpty(mov.LuaName)) + instanceMoveableProps[mov.LuaName] = mov.LuaProperties; + } + else if (obj is StaticInstance stat && stat.LuaProperties != null && stat.LuaProperties.HasProperties) + { + if (!string.IsNullOrEmpty(stat.LuaName)) + instanceStaticProps[stat.LuaName] = stat.LuaProperties; + } + } + } + + // Only build the script if there are any properties + bool hasAnyProperties = globalMoveableProps.Count > 0 || globalStaticProps.Count > 0 || + instanceMoveableProps.Count > 0 || instanceStaticProps.Count > 0; + + if (!hasAnyProperties) + { + _luaPropertyScript = string.Empty; + return; + } + + // Generate the Lua script text (will be embedded in the level file) + _luaPropertyScript = LuaPropertyScriptBuilder.BuildFullPropertyScript( + globalMoveableProps, globalStaticProps, + instanceMoveableProps, instanceStaticProps); + + ReportProgress(45, "Built Lua property script (" + _luaPropertyScript.Length + " chars)"); + } + 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..d1445aade9 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/TombEngine.cs @@ -113,6 +113,9 @@ private void WriteLevelTombEngine() set.Write(writer, _level.Settings.VolumeEventSets); } + // Write Lua property script blob + writer.Write(_luaPropertyScript); + dynamicDataBuffer = dynamicDataStream.ToArray(); } diff --git a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs index 9f1ccd824b..b60861cffe 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 04ba6a8d81..44a5ff5def 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; @@ -1266,6 +1267,25 @@ private static bool LoadObjects(ChunkReader chunkIO, ChunkId idOuter, LevelSetti addObject(instance); newObjects.TryAdd(objectID, instance); } + else if (id3 == Prj2Chunks.ObjectMovableTombEngine3) + { + var instance = new MoveableInstance(); + instance.Position = chunkIO.Raw.ReadVector3(); + instance.RotationY = chunkIO.Raw.ReadSingle(); + instance.RotationX = chunkIO.Raw.ReadSingle(); + instance.Roll = chunkIO.Raw.ReadSingle(); + ReadOptionalLEB128Int(chunkIO.Raw); + instance.WadObjectId = new WadMoveableId(chunkIO.Raw.ReadUInt32()); + instance.Ocb = chunkIO.Raw.ReadInt16(); + instance.Invisible = chunkIO.Raw.ReadBoolean(); + instance.ClearBody = chunkIO.Raw.ReadBoolean(); + instance.CodeBits = chunkIO.Raw.ReadByte(); + instance.Color = chunkIO.Raw.ReadVector3(); + instance.LuaName = chunkIO.Raw.ReadStringUTF8(); + ReadLuaProperties(chunkIO, instance.LuaProperties); + addObject(instance); + newObjects.TryAdd(objectID, instance); + } else if (id3 == Prj2Chunks.ObjectStatic || id3 == Prj2Chunks.ObjectStatic2) { @@ -1321,6 +1341,23 @@ private static bool LoadObjects(ChunkReader chunkIO, ChunkId idOuter, LevelSetti instance.LuaName = chunkIO.Raw.ReadStringUTF8(); addObject(instance); } + else if (id3 == Prj2Chunks.ObjectStaticTombEngine3) + { + var instance = new StaticInstance(); + newObjects.TryAdd(objectID, instance); + instance.Position = chunkIO.Raw.ReadVector3(); + instance.RotationY = chunkIO.Raw.ReadSingle(); + chunkIO.Raw.ReadSingle(); // Reserved: instance.RotationX + chunkIO.Raw.ReadSingle(); // Reserved: instance.Roll + instance.Scale = chunkIO.Raw.ReadSingle(); + ReadOptionalLEB128Int(chunkIO.Raw); + instance.WadObjectId = new WadStaticId(chunkIO.Raw.ReadUInt32()); + instance.Color = chunkIO.Raw.ReadVector3(); + instance.Ocb = chunkIO.Raw.ReadInt16(); + instance.LuaName = chunkIO.Raw.ReadStringUTF8(); + ReadLuaProperties(chunkIO, instance.LuaProperties); + addObject(instance); + } else if (id3 == Prj2Chunks.ObjectCamera) { var instance = new CameraInstance(); @@ -2016,6 +2053,24 @@ private static TriggerNode LoadNode(ChunkReader chunkIO, TriggerNode previous = return (uint)read; } + /// + /// Reads LuaPropertyContainer data from a flat chunk. + /// Format: int32 count, then for each property: UTF8 name + UTF8 value. + /// + 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 0bb1cadd9b..4712c98771 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 @@ -554,7 +555,7 @@ private static void WriteObjects(ChunkWriter chunkIO, IEnumerable + /// Writes LuaPropertyContainer data inline in a flat chunk. + /// Format: int32 count, then for each property: UTF8 name + UTF8 value. + /// + private static void WriteLuaProperties(ChunkWriter chunkIO, LuaPropertyContainer container) + { + if (container == null || !container.HasProperties) + { + chunkIO.Raw.Write((int)0); + return; + } + + var props = container.GetAll().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..c8a2240ec5 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,11 @@ public float Roll set { _roll = value; } } private float _roll = 0.0f; + + /// + /// Level 2 (per-instance) Lua property container. Stored in prj2 files. + /// Holds per-entity property overrides in boxed Lua format. + /// + 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..a538d1687f 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,11 @@ public float Scale set { _scale = value; } } private float _scale = 1.0f; + + /// + /// Level 2 (per-instance) Lua property container. Stored in prj2 files. + /// Holds per-entity property overrides in boxed Lua format. + /// + 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..af8f927a1f --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs @@ -0,0 +1,286 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using NLog; + +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. + /// Thread-safe via lazy initialization. + /// + 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. + /// + private static void ParseObjectNode(XmlNode objectNode, LuaPropertyObjectKind kind, + string filePath, Dictionary> result) + { + var idAttr = objectNode.Attributes?["id"]; + if (idAttr == null || !uint.TryParse(idAttr.Value, out uint typeId)) + { + logger.Warn("Property catalog entry missing or invalid 'id' attribute in {0}", filePath); + return; + } + + var key = new LuaPropertyObjectKey(kind, typeId); + + if (!result.ContainsKey(key)) + result[key] = new List(); + + foreach (XmlNode propNode in objectNode.SelectNodes("property")) + { + var definition = ParsePropertyNode(propNode, filePath); + if (definition == null || !definition.IsValid) + continue; + + // 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 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 = 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: default value + var defaultStr = propNode.Attributes?["default"]?.Value?.Trim() ?? string.Empty; + if (!string.IsNullOrEmpty(defaultStr)) + { + if (LuaValueParser.ValidateBoxedValue(propertyType, defaultStr)) + { + definition.DefaultValue = defaultStr; + } + else + { + logger.Warn("Property '{0}' has mismatched default value '{1}' for type {2} in {3}, using type default", + definition.InternalName, defaultStr, propertyType, filePath); + definition.DefaultValue = LuaValueParser.GetDefaultBoxedValue(propertyType); + } + } + 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..5c779edee0 --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyContainer.cs @@ -0,0 +1,116 @@ +// 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 representation so they can be written +// directly to Lua script blobs during level compilation. + +using System; +using System.Collections.Generic; +using System.Linq; + +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..7683e8854c --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs @@ -0,0 +1,59 @@ +// 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; + + /// + /// 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..be9820f82c --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyScriptBuilder.cs @@ -0,0 +1,181 @@ +// 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. + +using System.Collections.Generic; +using System.Text; +using TombLib.Utils; + +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 string BuildFullPropertyScript( + Dictionary globalMoveableProperties, + Dictionary globalStaticProperties, + Dictionary instanceMoveableProperties, + Dictionary instanceStaticProperties) + { + var sb = new StringBuilder(); + sb.AppendLine("-- Auto-generated property assignments (Tomb Editor)"); + sb.AppendLine("-- Level 1: Global object type properties"); + + // 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)); + } + } + } + + // 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)); + } + } + } + + sb.AppendLine(); + sb.AppendLine("-- Level 2: Instance properties"); + + // 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)); + } + } + } + + // 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)); + } + } + } + + return sb.ToString(); + } + } +} diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyType.cs b/TombLib/TombLib/LuaProperties/LuaPropertyType.cs new file mode 100644 index 0000000000..1a1e878fd1 --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaPropertyType.cs @@ -0,0 +1,21 @@ +// 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. + +namespace TombLib.LuaProperties +{ + /// + /// Enumerates all supported Lua property types for the TombEngine property grid system. + /// + public enum LuaPropertyType + { + Bool, + Int, + Float, + String, + Vec2, + Vec3, + Rotation, + Color, + Time + } +} diff --git a/TombLib/TombLib/LuaProperties/LuaValueParser.cs b/TombLib/TombLib/LuaProperties/LuaValueParser.cs new file mode 100644 index 0000000000..6bdd3bc421 --- /dev/null +++ b/TombLib/TombLib/LuaProperties/LuaValueParser.cs @@ -0,0 +1,510 @@ +// Shared Lua value boxing/unboxing abstraction. +// Extracted from ArgumentEditor to avoid duplicated parsing logic. +// Used by both the visual scripting ArgumentEditor and the new property grid system. + +using System; +using System.Globalization; +using System.Linq; +using NLog; +using TombLib.Utils; + +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) + + /// + /// Boxes a boolean value to its Lua representation ("true"/"false"). + /// + public static string BoxBool(bool value) + => value ? "true" : "false"; + + /// + /// Boxes an integer value to its Lua representation. + /// + public static string BoxInt(int value) + => value.ToString(Inv); + + /// + /// Boxes a float value to its Lua representation. + /// + public static string BoxFloat(float value) + => value.ToString(Inv); + + /// + /// Boxes a string value to its Lua representation (quoted with escaped inner quotes). + /// + public static string BoxString(string value) + => TextExtensions.Quote(TextExtensions.EscapeQuotes(value ?? string.Empty)); + + /// + /// Boxes a Vec2 value: TEN.Vec2(x, y) + /// + public static string BoxVec2(float x, float y) + => LuaSyntax.Vec2TypePrefix + LuaSyntax.BracketOpen + + x.ToString(Inv) + LuaSyntax.Separator + + y.ToString(Inv) + LuaSyntax.BracketClose; + + /// + /// Boxes a Vec3 value: TEN.Vec3(x, y, z) + /// + public static string BoxVec3(float x, float y, float z) + => LuaSyntax.Vec3TypePrefix + LuaSyntax.BracketOpen + + x.ToString(Inv) + LuaSyntax.Separator + + y.ToString(Inv) + LuaSyntax.Separator + + z.ToString(Inv) + LuaSyntax.BracketClose; + + /// + /// Boxes a Rotation value: TEN.Rotation(x, y, z) + /// + public static string BoxRotation(float x, float y, float z) + => LuaSyntax.RotationTypePrefix + LuaSyntax.BracketOpen + + x.ToString(Inv) + LuaSyntax.Separator + + y.ToString(Inv) + LuaSyntax.Separator + + z.ToString(Inv) + LuaSyntax.BracketClose; + + /// + /// Boxes a Color value: TEN.Color(r, g, b) or TEN.Color(r, g, b, a) + /// + 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; + } + + /// + /// Boxes a Time value: TEN.Time({h, m, s, cs}) + /// + public static string BoxTime(int hours, int minutes, int seconds, int centiseconds) + => 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) + + /// + /// Unboxes a boolean value from Lua text. + /// Supports "true", "false", and numeric strings (0 = false, non-zero = true). + /// + 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; + } + + /// + /// Unboxes an integer value from Lua text. + /// + 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; + } + + /// + /// Unboxes a float value from Lua text. + /// + 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; + } + + /// + /// Unboxes a string value from Lua text (removes surrounding quotes and unescapes). + /// + public static string UnboxString(string source, string defaultValue = "") + { + if (source == null) + return defaultValue; + + return TextExtensions.UnescapeQuotes(TextExtensions.Unquote(source)); + } + + /// + /// Unboxes a Vec2 value from Lua text: TEN.Vec2(x, y) → float[2]. + /// + 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]) + }; + } + + /// + /// Unboxes a Vec3 value from Lua text: TEN.Vec3(x, y, z) → float[3]. + /// + 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]) + }; + } + + /// + /// Unboxes a Rotation value from Lua text: TEN.Rotation(x, y, z) → float[3]. + /// Values are clamped to 0–360 range. + /// + 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])) + }; + } + + /// + /// Unboxes a Color value from Lua text: TEN.Color(r, g, b[, a]) → byte[3 or 4]. + /// + 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 }; + } + + /// + /// Unboxes a Time value from Lua text: TEN.Time({h, m, s, cs}) → int[4]. + /// + public static int[] UnboxTime(string source) + { + var defaults = new int[] { 0, 0, 0, 0 }; + if (string.IsNullOrWhiteSpace(source)) + return defaults; + + // Strip TEN.Time( ... ) wrapper and { } table braces + 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); + + 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); + 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); + + 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" + /// + internal 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. + /// + internal 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..2a8618cfc8 100644 --- a/TombLib/TombLib/TombLib.csproj +++ b/TombLib/TombLib/TombLib.csproj @@ -479,6 +479,9 @@ 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..7062a0fb2a 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,27 @@ public static void WriteMetadata(ChunkWriter chunkIO, Wad2 wad) chunkIO.WriteChunkString(Wad2Chunks.UserNotes, wad.UserNotes); }); } + + /// + /// Writes a Lua property container as a child chunk within a moveable or static chunk. + /// Properties are stored as name/value string pairs in boxed Lua format. + /// + private static void WriteLuaProperties(ChunkWriter chunkIO, LuaProperties.LuaPropertyContainer container) + { + if (container == null || !container.HasProperties) + return; + + chunkIO.WriteChunkWithChildren(Wad2Chunks.LuaProperties, () => + { + foreach (var prop in container.GetAll()) + { + 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..8fe57300f8 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; @@ -79,6 +80,12 @@ public class WadMoveable : IWadObject //public WadBone Skeleton { get; set; } = new WadBone(); public List Bones { get; } = new List(); + /// + /// Level 1 (global) Lua property container. Stored in wad2 files. + /// Holds per-object-type property overrides in boxed Lua format. + /// + public LuaPropertyContainer LuaProperties { get; set; } = new LuaPropertyContainer(); + public WadMoveable(WadMoveableId id) { Id = id; @@ -92,6 +99,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..45c67fdeb9 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,12 @@ public WadStatic(WadStaticId id) public bool Shatter { get; set; } = false; public int ShatterSoundID { get; set; } = -1; + /// + /// Level 1 (global) Lua property container. Stored in wad2 files. + /// Holds per-object-type property overrides in boxed Lua format. + /// + 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 +70,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/Forms/FormLuaProperties.cs b/WadTool/Forms/FormLuaProperties.cs new file mode 100644 index 0000000000..ddd7a5e7d7 --- /dev/null +++ b/WadTool/Forms/FormLuaProperties.cs @@ -0,0 +1,224 @@ +using DarkUI.Forms; +using System; +using System.Drawing; +using System.Windows.Forms; +using System.Windows.Forms.Integration; +using TombLib.Forms.ViewModels; +using TombLib.Forms.Views; +using TombLib.LuaProperties; +using TombLib.Wad; + +namespace WadTool +{ + public partial class FormLuaProperties : DarkForm + { + private readonly WadToolClass _tool; + private readonly Wad2 _wad; + private readonly IWadObject _wadObject; + private readonly IWadObjectId _objectId; + + // WPF hosting + private readonly ElementHost _elementHost; + private readonly LuaPropertyGridControl _wpfControl; + private readonly LuaPropertyGridViewModel _viewModel; + + // Original container for cancel/restore + private readonly LuaPropertyContainer _originalProperties; + + public FormLuaProperties(WadToolClass tool, Wad2 wad, IWadObjectId objectId, IWadObject wadObject) + { + _tool = tool; + _wad = wad; + _wadObject = wadObject; + _objectId = objectId; + + InitializeForm(); + + // Determine kind and type ID + LuaPropertyObjectKind kind; + uint typeId; + string objectName; + + if (wadObject is WadMoveable moveable) + { + kind = LuaPropertyObjectKind.Moveable; + typeId = ((WadMoveableId)objectId).TypeId; + objectName = TombLib.Wad.Catalog.TrCatalog.GetMoveableName(wad.GameVersion, typeId); + } + else if (wadObject is WadStatic staticObj) + { + kind = LuaPropertyObjectKind.Static; + typeId = ((WadStaticId)objectId).TypeId; + objectName = TombLib.Wad.Catalog.TrCatalog.GetStaticName(wad.GameVersion, typeId); + } + else + { + // Unsupported object type + return; + } + + Text = $"Lua Properties - {objectName} (Slot {typeId})"; + + // Save original state for undo on cancel + _originalProperties = GetContainer().Clone(); + + // Create WPF control + view model + _viewModel = new LuaPropertyGridViewModel(); + _viewModel.Title = objectName; + + var definitions = LuaPropertyCatalog.GetDefinitions(kind, typeId); + _viewModel.Load(definitions, GetContainer()); + + _wpfControl = new LuaPropertyGridControl(); + _wpfControl.ViewModel = _viewModel; + + _elementHost = new ElementHost + { + Dock = DockStyle.Fill, + Child = _wpfControl + }; + + panelContent.Controls.Add(_elementHost); + + if (definitions.Count == 0) + { + lblNoProperties.Visible = true; + lblNoProperties.BringToFront(); + } + } + + private LuaPropertyContainer GetContainer() + { + if (_wadObject is WadMoveable moveable) + return moveable.LuaProperties; + if (_wadObject is WadStatic staticObj) + return staticObj.LuaProperties; + return null; + } + + private void InitializeForm() + { + SuspendLayout(); + + // Form settings + Name = "FormLuaProperties"; + Size = new Size(420, 520); + MinimumSize = new Size(350, 300); + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.Sizable; + MaximizeBox = false; + MinimizeBox = false; + ShowIcon = false; + ShowInTaskbar = false; + + // Bottom button panel + panelButtons = new Panel + { + Dock = DockStyle.Bottom, + Height = 40, + Padding = new Padding(6, 6, 6, 6) + }; + + butCancel = new DarkUI.Controls.DarkButton + { + Text = "Cancel", + DialogResult = DialogResult.Cancel, + Dock = DockStyle.Right, + Width = 80, + Padding = new Padding(0) + }; + butCancel.Click += ButCancel_Click; + + butOK = new DarkUI.Controls.DarkButton + { + Text = "OK", + Dock = DockStyle.Right, + Width = 80, + Padding = new Padding(0) + }; + butOK.Click += ButOK_Click; + + butReset = new DarkUI.Controls.DarkButton + { + Text = "Reset All", + Dock = DockStyle.Left, + Width = 80, + Padding = new Padding(0) + }; + butReset.Click += ButReset_Click; + + panelButtons.Controls.Add(butCancel); + panelButtons.Controls.Add(butOK); + panelButtons.Controls.Add(butReset); + + // Content panel for the WPF control + panelContent = new Panel + { + Dock = DockStyle.Fill + }; + + // Label for when no properties exist + lblNoProperties = new DarkUI.Controls.DarkLabel + { + Text = "No Lua properties defined for this object type.\n\nAdd property definitions in:\nCatalogs/TEN Property Catalogs/", + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleCenter, + Visible = false, + AutoSize = false + }; + panelContent.Controls.Add(lblNoProperties); + + Controls.Add(panelContent); + Controls.Add(panelButtons); + + AcceptButton = butOK; + CancelButton = butCancel; + + ResumeLayout(false); + } + + private void ButOK_Click(object sender, EventArgs e) + { + // Values are already written to the container via the ViewModel's live-write. + // Just mark the wad as changed and close. + _tool.ToggleUnsavedChanges(); + DialogResult = DialogResult.OK; + Close(); + } + + private void ButCancel_Click(object sender, EventArgs e) + { + // Restore original properties + var container = GetContainer(); + if (container != null) + { + container.Clear(); + foreach (var kvp in _originalProperties.GetAll()) + container.SetValue(kvp.Key, kvp.Value); + } + + DialogResult = DialogResult.Cancel; + Close(); + } + + private void ButReset_Click(object sender, EventArgs e) + { + _viewModel.ResetAll(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _elementHost?.Dispose(); + base.Dispose(disposing); + } + + // Controls + private Panel panelButtons; + private Panel panelContent; + private DarkUI.Controls.DarkButton butOK; + private DarkUI.Controls.DarkButton butCancel; + private DarkUI.Controls.DarkButton butReset; + private DarkUI.Controls.DarkLabel lblNoProperties; + } +} diff --git a/WadTool/Forms/FormMain.Designer.cs b/WadTool/Forms/FormMain.Designer.cs index 1531bf13cf..7e49694469 100644 --- a/WadTool/Forms/FormMain.Designer.cs +++ b/WadTool/Forms/FormMain.Designer.cs @@ -99,6 +99,7 @@ private void InitializeComponent() butEditSkeleton = new System.Windows.Forms.ToolStripButton(); butEditStaticModel = new System.Windows.Forms.ToolStripButton(); butEditSpriteSequence = new System.Windows.Forms.ToolStripButton(); + butEditLuaProperties = new System.Windows.Forms.ToolStripButton(); contextMenuMoveableItem = new DarkUI.Controls.DarkContextMenu(); editAnimationsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); editSkeletonToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -933,7 +934,7 @@ private void InitializeComponent() darkToolStrip2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); darkToolStrip2.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); darkToolStrip2.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden; - darkToolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { butEditAnimations, butEditSkeleton, butEditStaticModel, butEditSpriteSequence }); + darkToolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { butEditAnimations, butEditSkeleton, butEditStaticModel, butEditSpriteSequence, butEditLuaProperties }); darkToolStrip2.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.HorizontalStackWithOverflow; darkToolStrip2.Location = new System.Drawing.Point(0, 631); darkToolStrip2.Name = "darkToolStrip2"; @@ -986,6 +987,17 @@ private void InitializeComponent() butEditSpriteSequence.Text = "Edit sprite sequence"; butEditSpriteSequence.Click += butEditSpriteSequence_Click; // + // butEditLuaProperties + // + butEditLuaProperties.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); + butEditLuaProperties.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); + butEditLuaProperties.Image = Properties.Resources.edit_16; + butEditLuaProperties.ImageTransparentColor = System.Drawing.Color.Magenta; + butEditLuaProperties.Name = "butEditLuaProperties"; + butEditLuaProperties.Size = new System.Drawing.Size(115, 24); + butEditLuaProperties.Text = "Lua Properties"; + butEditLuaProperties.Click += butEditLuaProperties_Click; + // // contextMenuMoveableItem // contextMenuMoveableItem.BackColor = System.Drawing.Color.FromArgb(60, 63, 65); @@ -1209,6 +1221,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripButton butEditAnimations; private System.Windows.Forms.ToolStripButton butEditStaticModel; private System.Windows.Forms.ToolStripButton butEditSpriteSequence; + private System.Windows.Forms.ToolStripButton butEditLuaProperties; private DarkUI.Controls.DarkSectionPanel darkSectionPanel3; private DarkUI.Controls.DarkToolStrip darkToolStrip3; private System.Windows.Forms.ToolStripButton toolStripButton1; diff --git a/WadTool/Forms/FormMain.cs b/WadTool/Forms/FormMain.cs index 59aedd990d..d47edb2aff 100644 --- a/WadTool/Forms/FormMain.cs +++ b/WadTool/Forms/FormMain.cs @@ -134,6 +134,7 @@ obj is WadToolClass.SourceWadChangedEvent || butEditSkeleton.Visible = false; butEditStaticModel.Visible = false; butEditSpriteSequence.Visible = false; + butEditLuaProperties.Visible = false; } else { @@ -157,6 +158,8 @@ obj is WadToolClass.SourceWadChangedEvent || butEditSkeleton.Visible = (mainSelection.Value.Id is WadMoveableId); butEditStaticModel.Visible = (mainSelection.Value.Id is WadStaticId); butEditSpriteSequence.Visible = (mainSelection.Value.Id is WadSpriteSequenceId); + butEditLuaProperties.Visible = (mainSelection.Value.Id is WadMoveableId || mainSelection.Value.Id is WadStaticId) && + mainSelection.Value.WadArea == WadArea.Destination; panel3D.ResetCamera(); panel3D.Invalidate(); @@ -551,6 +554,11 @@ private void butEditSpriteSequence_Click(object sender, EventArgs e) WadActions.EditObject(_tool, this, DeviceManager.DefaultDeviceManager); } + private void butEditLuaProperties_Click(object sender, EventArgs e) + { + WadActions.EditLuaProperties(_tool, this); + } + private void changeSlotToolStripMenuItem_Click(object sender, EventArgs e) { butChangeSlot_Click(null, null); diff --git a/WadTool/WadActions.cs b/WadTool/WadActions.cs index afedd1a567..bad7b24f6b 100644 --- a/WadTool/WadActions.cs +++ b/WadTool/WadActions.cs @@ -965,6 +965,26 @@ public static void EditObject(WadToolClass tool, IWin32Window owner, DeviceManag } } + public static void EditLuaProperties(WadToolClass tool, IWin32Window owner) + { + Wad2 wad = tool.GetWad(tool.MainSelection?.WadArea); + var wadObject = wad?.TryGet(tool.MainSelection?.Id); + + if (wadObject == null || tool.MainSelection?.WadArea != WadArea.Destination) + return; + + if (!(wadObject is WadMoveable) && !(wadObject is WadStatic)) + { + tool.SendMessage("Lua properties are only available for moveables and statics.", PopupType.Info); + return; + } + + using (var form = new FormLuaProperties(tool, wad, tool.MainSelection.Value.Id, wadObject)) + { + 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 From 49c3c3c8cebe77664cf7d834d8d5dec4bc061d83 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:35:57 +0100 Subject: [PATCH 02/30] Second iteration --- TombEditor/Configuration.cs | 7 + .../ToolWindows/LuaProperties.Designer.cs | 1 + .../ViewModels/LuaPropertyGridViewModel.cs | 21 +- .../ViewModels/LuaPropertyRowViewModel.cs | 19 +- .../Views/LuaPropertyGridControl.xaml | 189 +++++++++++------- .../Views/LuaPropertyGridControl.xaml.cs | 16 ++ .../TEN Property Catalogs/Example.xml | 8 +- .../LuaProperties/LuaPropertyCatalog.cs | 120 +++++++++-- .../LuaProperties/LuaPropertyDefinition.cs | 7 + 9 files changed, 275 insertions(+), 113 deletions(-) diff --git a/TombEditor/Configuration.cs b/TombEditor/Configuration.cs index f967c6d582..a1148b475c 100644 --- a/TombEditor/Configuration.cs +++ b/TombEditor/Configuration.cs @@ -351,6 +351,13 @@ public void EnsureDefaults() VisibleContent = "TriggerList", Order = 3, Size = new Size(284,174) + }, + new DockGroupState + { + Contents = new List { "LuaProperties" }, + VisibleContent = "LuaProperties", + Order = 4, + Size = new Size(284,200) } } }, diff --git a/TombEditor/ToolWindows/LuaProperties.Designer.cs b/TombEditor/ToolWindows/LuaProperties.Designer.cs index d905ffa889..0092b700e3 100644 --- a/TombEditor/ToolWindows/LuaProperties.Designer.cs +++ b/TombEditor/ToolWindows/LuaProperties.Designer.cs @@ -21,6 +21,7 @@ private void InitializeComponent() // this.DockText = "Lua Properties"; this.Name = "LuaProperties"; + this.SerializationKey = "LuaProperties"; this.Size = new System.Drawing.Size(300, 400); this.ResumeLayout(false); } diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs index a2ddb34e2d..da40a9231c 100644 --- a/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs @@ -5,9 +5,9 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Linq; -using System.Runtime.CompilerServices; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using TombLib.LuaProperties; namespace TombLib.Forms.ViewModels @@ -17,7 +17,7 @@ namespace TombLib.Forms.ViewModels /// Displays property definitions for a specific object type, populated /// with current values from a . /// - public class LuaPropertyGridViewModel : INotifyPropertyChanged + public partial class LuaPropertyGridViewModel : ObservableObject { /// /// All property rows, grouped by category. @@ -48,11 +48,7 @@ public class LuaPropertyGridViewModel : INotifyPropertyChanged /// /// The display title for the property grid header. /// - public string Title - { - get => _title; - set { _title = value; OnPropertyChanged(); } - } + [ObservableProperty] private string _title = "Properties"; /// @@ -136,6 +132,7 @@ public LuaPropertyContainer ToContainer() /// /// Resets all properties to their default values. /// + [RelayCommand] public void ResetAll() { foreach (var row in Properties) @@ -171,13 +168,5 @@ private void OnRowValueChanged(object sender, EventArgs e) PropertyValueChanged?.Invoke(sender, e); } - - #region INotifyPropertyChanged - - public event PropertyChangedEventHandler PropertyChanged; - private void OnPropertyChanged([CallerMemberName] string name = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - - #endregion } } diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs index 6892a99d95..bae1f22a68 100644 --- a/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs @@ -2,9 +2,8 @@ // Manages the binding between a LuaPropertyDefinition and its current boxed value. using System; -using System.ComponentModel; using System.Globalization; -using System.Runtime.CompilerServices; +using CommunityToolkit.Mvvm.ComponentModel; using TombLib.LuaProperties; namespace TombLib.Forms.ViewModels @@ -13,7 +12,7 @@ namespace TombLib.Forms.ViewModels /// ViewModel for a single row in the Lua property grid. /// Wraps a with an editable current value. /// - public class LuaPropertyRowViewModel : INotifyPropertyChanged + public partial class LuaPropertyRowViewModel : ObservableObject { private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; @@ -39,6 +38,11 @@ public class LuaPropertyRowViewModel : INotifyPropertyChanged /// 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. /// @@ -221,13 +225,6 @@ public void ResetToDefault() public bool IsModified => !string.Equals(_boxedValue, Definition.DefaultValue, StringComparison.Ordinal); - #region INotifyPropertyChanged - - public event PropertyChangedEventHandler PropertyChanged; - - private void OnPropertyChanged([CallerMemberName] string name = null) - => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); - /// /// Notifies all typed property bindings that the underlying value changed. /// @@ -276,7 +273,5 @@ private void RefreshTypedProperties() break; } } - - #endregion } } diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml index 2b1530fe48..219f6ccaed 100644 --- a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml @@ -10,6 +10,7 @@ xmlns:vm="clr-namespace:TombLib.Forms.ViewModels" d:DataContext="{d:DesignInstance Type=vm:LuaPropertyGridViewModel}" Background="{DynamicResource Brush_Background}" + Foreground="{DynamicResource Brush_Text}" mc:Ignorable="d"> @@ -22,10 +23,10 @@ @@ -59,7 +60,7 @@ HorizontalAlignment="Stretch" /> - + @@ -68,16 +69,22 @@ - - + + - + - + @@ -88,19 +95,28 @@ - - + + - + - + - + @@ -111,15 +127,24 @@ - - + + - + - + @@ -129,18 +154,17 @@ - - - + + TextAlignment="Left" Margin="2,0,0,0" + Value="{Binding ColorA, Mode=TwoWay}" + Visibility="{Binding HasAlpha, Converter={StaticResource BoolToVisConverter}}" /> @@ -157,7 +181,7 @@ - + @@ -184,12 +208,37 @@ ColorTemplate="{StaticResource ColorTemplate}" TimeTemplate="{StaticResource TimeTemplate}" /> - - + + + + + + + + + @@ -209,40 +258,44 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs index f9b8160a52..3fdd7c4906 100644 --- a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml.cs @@ -7,6 +7,7 @@ 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; @@ -82,6 +83,21 @@ private void ColorPicker_Click(object sender, RoutedEventArgs e) } } } + + /// + /// Handles double-click on the property name label. + /// Resets the property value to its default. + /// + private void PropertyName_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.ClickCount == 2 && + sender is FrameworkElement element && + element.DataContext is LuaPropertyRowViewModel row) + { + row.ResetToDefault(); + e.Handled = true; + } + } } /// diff --git a/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml b/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml index 755714c0f7..e2f7f403bf 100644 --- a/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml +++ b/TombLib/TombLib/Catalogs/TEN Property Catalogs/Example.xml @@ -3,7 +3,13 @@ TEN Lua Property Catalog - Example This file defines custom Lua properties for moveable and static object types. - Each or element targets an object slot by its numeric type ID. + Each or element targets object slots by numeric type ID. + + Supported id formats: + - Single: id="73" + - List: id="73,74,75" + - Range: id="73-80" + - Mixed: id="0-5,73,100-105" Supported types: Bool, Int, Float, String, Vec2, Vec3, Rotation, Color, Time diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs b/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs index af8f927a1f..744902e899 100644 --- a/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs +++ b/TombLib/TombLib/LuaProperties/LuaPropertyCatalog.cs @@ -2,6 +2,7 @@ // 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". using System; using System.Collections.Generic; @@ -172,37 +173,117 @@ private static void LoadCatalogFile(string filePath, Dictionary /// 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 || !uint.TryParse(idAttr.Value, out uint typeId)) + if (idAttr == null || string.IsNullOrWhiteSpace(idAttr.Value)) { - logger.Warn("Property catalog entry missing or invalid 'id' attribute in {0}", filePath); + logger.Warn("Property catalog entry missing 'id' attribute in {0}", filePath); return; } - var key = new LuaPropertyObjectKey(kind, typeId); - - if (!result.ContainsKey(key)) - result[key] = new List(); + 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) - continue; + if (definition != null && definition.IsValid) + definitions.Add(definition); + } - // If same internal name exists, replace it (latest file wins). - var existingIndex = result[key].FindIndex(p => - string.Equals(p.InternalName, definition.InternalName, StringComparison.OrdinalIgnoreCase)); + 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); + } + } + } - if (existingIndex >= 0) - result[key][existingIndex] = 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 - result[key].Add(definition); + { + // 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; } /// @@ -240,8 +321,15 @@ private static LuaPropertyDefinition ParsePropertyNode(XmlNode propNode, string } definition.Type = propertyType; - // Optional: default value - var defaultStr = propNode.Attributes?["default"]?.Value?.Trim() ?? string.Empty; + // 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: default value (accept both "defaultValue" and "default" attribute names) + var defaultStr = (propNode.Attributes?["defaultValue"]?.Value + ?? propNode.Attributes?["default"]?.Value)?.Trim() + ?? string.Empty; if (!string.IsNullOrEmpty(defaultStr)) { if (LuaValueParser.ValidateBoxedValue(propertyType, defaultStr)) diff --git a/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs b/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs index 7683e8854c..3647668c04 100644 --- a/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs +++ b/TombLib/TombLib/LuaProperties/LuaPropertyDefinition.cs @@ -46,6 +46,13 @@ public class LuaPropertyDefinition /// 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; + /// /// Returns true if the definition has all required fields filled in. /// From 7382b6c004349337724a4835b1bec74fea23dc42 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:47:21 +0100 Subject: [PATCH 03/30] Fixed text control focus --- TombLib/TombLib.Forms/Utils/WinFormsUtils.cs | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs b/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs index 1e5dcb2153..836da45308 100644 --- a/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs +++ b/TombLib/TombLib.Forms/Utils/WinFormsUtils.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using System.Windows.Forms; +using System.Windows.Forms.Integration; namespace TombLib.Utils { @@ -125,9 +126,24 @@ public static IEnumerable BoolCombine(IEnumerable oldObjects, IEnumerab public static bool CurrentControlSupportsInput(Form form, Keys keyData) { - var activeControlType = GetFocusedControl(form)?.GetType().Name; - - if ((keyData.HasFlag(Keys.Control | Keys.A) || + var activeControl = GetFocusedControl(form); + var activeControlType = activeControl?.GetType().Name; + + 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) || From 1e619ca00c66ebcff7c3890f3d412d4d7cd182c2 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:52:24 +0100 Subject: [PATCH 04/30] Fix WPF nud not accepting minus key --- DarkUI/DarkUI.WPF/CustomControls/NumericUpDown.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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; } From e8bcd38ffaa5966b92dbfbda0d6bcfdc07e2aaa8 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:08:00 +0100 Subject: [PATCH 05/30] Fixed tooltip style --- .../Views/LuaPropertyGridControl.xaml | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml index 219f6ccaed..3c17186f09 100644 --- a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml @@ -240,6 +240,30 @@ + + + @@ -273,7 +297,11 @@ - + + + + From b238133fc12c98e6f3b7437b5ecc4b256451462a Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:31:48 +0100 Subject: [PATCH 06/30] Renames --- TombEditor/Command.cs | 2 +- TombEditor/Configuration.cs | 4 ++-- TombEditor/Forms/FormMain.Designer.cs | 4 ++-- TombEditor/Forms/FormMain.cs | 4 ++-- ...ies.Designer.cs => ItemProperties.Designer.cs} | 10 +++++----- .../{LuaProperties.cs => ItemProperties.cs} | 15 ++++++++++----- .../ViewModels/LuaPropertyGridViewModel.cs | 6 ++++++ .../Views/LuaPropertyGridControl.xaml | 2 +- WadTool/Forms/FormLuaProperties.cs | 4 ++-- WadTool/Forms/FormMain.Designer.cs | 2 +- WadTool/WadActions.cs | 2 +- 11 files changed, 33 insertions(+), 22 deletions(-) rename TombEditor/ToolWindows/{LuaProperties.Designer.cs => ItemProperties.Designer.cs} (76%) rename TombEditor/ToolWindows/{LuaProperties.cs => ItemProperties.cs} (87%) diff --git a/TombEditor/Command.cs b/TombEditor/Command.cs index 66f1d97508..868add0029 100644 --- a/TombEditor/Command.cs +++ b/TombEditor/Command.cs @@ -1685,7 +1685,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("ShowLuaProperties", "Show Lua properties", CommandType.Windows, (CommandArgs args) => args.Editor.ToggleToolWindow(typeof(LuaProperties))); + 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 a1148b475c..4edfa06bb7 100644 --- a/TombEditor/Configuration.cs +++ b/TombEditor/Configuration.cs @@ -354,8 +354,8 @@ public void EnsureDefaults() }, new DockGroupState { - Contents = new List { "LuaProperties" }, - VisibleContent = "LuaProperties", + Contents = new List { "ItemProperties" }, + VisibleContent = "ItemProperties", Order = 4, Size = new Size(284,200) } diff --git a/TombEditor/Forms/FormMain.Designer.cs b/TombEditor/Forms/FormMain.Designer.cs index b529f625d0..68fa055dfd 100644 --- a/TombEditor/Forms/FormMain.Designer.cs +++ b/TombEditor/Forms/FormMain.Designer.cs @@ -1977,8 +1977,8 @@ private void InitializeComponent() luaPropertiesToolStripMenuItem.ForeColor = System.Drawing.Color.FromArgb(220, 220, 220); luaPropertiesToolStripMenuItem.Name = "luaPropertiesToolStripMenuItem"; luaPropertiesToolStripMenuItem.Size = new System.Drawing.Size(246, 22); - luaPropertiesToolStripMenuItem.Tag = "ShowLuaProperties"; - luaPropertiesToolStripMenuItem.Text = "ShowLuaProperties"; + luaPropertiesToolStripMenuItem.Tag = "ShowItemProperties"; + luaPropertiesToolStripMenuItem.Text = "ShowItemProperties"; // // statisticsToolStripMenuItem // diff --git a/TombEditor/Forms/FormMain.cs b/TombEditor/Forms/FormMain.cs index f4ad8908a1..ad20a4fe5b 100644 --- a/TombEditor/Forms/FormMain.cs +++ b/TombEditor/Forms/FormMain.cs @@ -37,7 +37,7 @@ public partial class FormMain : DarkForm new TexturePanel(), new ObjectList(), new ToolPalette(), - new LuaProperties() + new ItemProperties() }; // Floating tool boxes are placed on 3D view at runtime @@ -565,7 +565,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()); + luaPropertiesToolStripMenuItem.Checked = dockArea.ContainsContent(GetWindow()); dockableToolStripMenuItem.Checked = dockArea.ContainsContent(GetWindow()); } diff --git a/TombEditor/ToolWindows/LuaProperties.Designer.cs b/TombEditor/ToolWindows/ItemProperties.Designer.cs similarity index 76% rename from TombEditor/ToolWindows/LuaProperties.Designer.cs rename to TombEditor/ToolWindows/ItemProperties.Designer.cs index 0092b700e3..3c6418bad6 100644 --- a/TombEditor/ToolWindows/LuaProperties.Designer.cs +++ b/TombEditor/ToolWindows/ItemProperties.Designer.cs @@ -1,6 +1,6 @@ namespace TombEditor.ToolWindows { - partial class LuaProperties + partial class ItemProperties { /// /// Required designer variable. @@ -17,11 +17,11 @@ private void InitializeComponent() { this.SuspendLayout(); // - // LuaProperties + // ItemProperties // - this.DockText = "Lua Properties"; - this.Name = "LuaProperties"; - this.SerializationKey = "LuaProperties"; + this.DockText = "Item Properties"; + this.Name = "ItemProperties"; + this.SerializationKey = "ItemProperties"; this.Size = new System.Drawing.Size(300, 400); this.ResumeLayout(false); } diff --git a/TombEditor/ToolWindows/LuaProperties.cs b/TombEditor/ToolWindows/ItemProperties.cs similarity index 87% rename from TombEditor/ToolWindows/LuaProperties.cs rename to TombEditor/ToolWindows/ItemProperties.cs index a584ec1256..b5710ce4e2 100644 --- a/TombEditor/ToolWindows/LuaProperties.cs +++ b/TombEditor/ToolWindows/ItemProperties.cs @@ -11,7 +11,7 @@ namespace TombEditor.ToolWindows { - public partial class LuaProperties : DarkToolWindow + public partial class ItemProperties : DarkToolWindow { private readonly Editor _editor; @@ -23,7 +23,7 @@ public partial class LuaProperties : DarkToolWindow // Tracked selection private ObjectInstance _currentObject; - public LuaProperties() + public ItemProperties() { InitializeComponent(); @@ -79,7 +79,8 @@ private void EditorEventRaised(IEditorEvent obj) // 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.LevelChangedEvent || + obj is Editor.InitEvent) { UpdatePropertyGrid(); } @@ -93,7 +94,8 @@ private void UpdatePropertyGrid() if (!_editor.Level.IsTombEngine) { _viewModel.Clear(); - _viewModel.Title = "Lua Properties"; + _viewModel.Title = "Item Properties"; + _viewModel.StatusMessage = "Not supported for this engine target."; _currentObject = null; return; } @@ -106,6 +108,7 @@ private void UpdatePropertyGrid() _viewModel.Title = $"Properties: {moveable.ItemType.ToString()}"; _viewModel.Load(definitions, moveable.LuaProperties); + _viewModel.StatusMessage = "No properties defined for this moveable type."; } else if (selected is StaticInstance staticObj) { @@ -115,12 +118,14 @@ private void UpdatePropertyGrid() _viewModel.Title = $"Properties: {staticObj.ItemType.ToString()}"; _viewModel.Load(definitions, staticObj.LuaProperties); + _viewModel.StatusMessage = "No properties defined for this static mesh slot."; } else { _currentObject = null; _viewModel.Clear(); - _viewModel.Title = "Lua Properties"; + _viewModel.Title = "Item Properties"; + _viewModel.StatusMessage = "Select an object to edit properties."; } } diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs index da40a9231c..27566fb558 100644 --- a/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyGridViewModel.cs @@ -45,6 +45,12 @@ public partial class LuaPropertyGridViewModel : ObservableObject /// public bool IsEmpty => Properties.Count == 0; + /// + /// 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. /// diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml index 3c17186f09..d4110e35d4 100644 --- a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml @@ -274,7 +274,7 @@ Date: Sun, 1 Mar 2026 19:42:51 +0100 Subject: [PATCH 07/30] Update LevelCompilerTombEngine.cs --- .../LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs index d5ace986a9..7f9a672735 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs @@ -619,8 +619,6 @@ private void BuildLuaPropertyScript() _luaPropertyScript = LuaPropertyScriptBuilder.BuildFullPropertyScript( globalMoveableProps, globalStaticProps, instanceMoveableProps, instanceStaticProps); - - ReportProgress(45, "Built Lua property script (" + _luaPropertyScript.Length + " chars)"); } public bool CheckTombEngineVersion() From dc294addd74afd73aec85235841a2b615c1e1d2f Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:51:56 +0100 Subject: [PATCH 08/30] Write default properties from the xml file, if not present in wads --- .../TombEngine/LevelCompilerTombEngine.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs index 7f9a672735..fd1ab0f479 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/LevelCompilerTombEngine.cs @@ -584,6 +584,45 @@ private void BuildLuaPropertyScript() } } + // Fall back to XML catalog defaults for object types that don't have + // global properties in wad2 (e.g. older wad2 files without property data). + 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) || globalMoveableProps.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); + globalMoveableProps[slotName] = container; + } + + foreach (var stat in wadRef.Wad.Statics) + { + if (globalStaticProps.ContainsKey(stat.Key.TypeId)) + continue; + + var definitions = LuaPropertyCatalog.GetDefinitions(LuaPropertyObjectKind.Static, stat.Key.TypeId); + if (definitions.Count == 0) + continue; + + var container = new LuaPropertyContainer(); + foreach (var def in definitions) + container.SetValue(def.InternalName, def.DefaultValue); + globalStaticProps[stat.Key.TypeId] = container; + } + } + // Collect Level 2 properties: per-instance from rooms var instanceMoveableProps = new Dictionary(); var instanceStaticProps = new Dictionary(); From 9b00175d72808ee056f4c2bde67c8348f2b49149 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:59:37 +0100 Subject: [PATCH 09/30] Added enum property type --- TombEditor/ToolWindows/ItemProperties.cs | 2 +- .../ViewModels/LuaPropertyRowViewModel.cs | 25 ++ .../LuaPropertyEditorTemplateSelector.cs | 18 +- .../Views/LuaPropertyGridControl.xaml | 10 +- .../TEN Property Catalogs/Example.xml | 31 ++- .../Catalogs/TEN Property Catalogs/Lara.xml | 217 --------------- .../Catalogs/TEN Property Catalogs/README.md | 263 ++++++++++++++++++ .../LuaProperties/LuaPropertyCatalog.cs | 41 +++ .../LuaProperties/LuaPropertyDefinition.cs | 9 + .../TombLib/LuaProperties/LuaPropertyType.cs | 5 +- .../TombLib/LuaProperties/LuaValueParser.cs | 24 +- TombLib/TombLib/TombLib.csproj | 2 +- 12 files changed, 407 insertions(+), 240 deletions(-) delete mode 100644 TombLib/TombLib/Catalogs/TEN Property Catalogs/Lara.xml create mode 100644 TombLib/TombLib/Catalogs/TEN Property Catalogs/README.md diff --git a/TombEditor/ToolWindows/ItemProperties.cs b/TombEditor/ToolWindows/ItemProperties.cs index b5710ce4e2..b07dafa9a7 100644 --- a/TombEditor/ToolWindows/ItemProperties.cs +++ b/TombEditor/ToolWindows/ItemProperties.cs @@ -125,7 +125,7 @@ private void UpdatePropertyGrid() _currentObject = null; _viewModel.Clear(); _viewModel.Title = "Item Properties"; - _viewModel.StatusMessage = "Select an object to edit properties."; + _viewModel.StatusMessage = "Select a valid object to edit properties."; } } diff --git a/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs index bae1f22a68..a62d80c2e9 100644 --- a/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs +++ b/TombLib/TombLib.Forms/ViewModels/LuaPropertyRowViewModel.cs @@ -2,6 +2,7 @@ // Manages the binding between a LuaPropertyDefinition and its current boxed value. using System; +using System.Collections.Generic; using System.Globalization; using CommunityToolkit.Mvvm.ComponentModel; using TombLib.LuaProperties; @@ -203,6 +204,27 @@ public int TimeCentiseconds set { var t = LuaValueParser.UnboxTime(_boxedValue); BoxedValue = LuaValueParser.BoxTime(t[0], t[1], t[2], value); } } + // --- Enum --- + /// + /// Ordered list of entry names for this Enum property (forwarded from the definition). + /// Index 0 maps to Lua integer value 0. + /// + public IReadOnlyList EnumValues => Definition.EnumValues; + + /// + /// 0-based selected index for ComboBox binding. + /// Stored directly as a 0-based integer in . + /// + 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 public LuaPropertyRowViewModel(LuaPropertyDefinition definition, string initialBoxedValue = null) @@ -271,6 +293,9 @@ private void RefreshTypedProperties() 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 index acb66e8cea..2afffbfd08 100644 --- a/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs +++ b/TombLib/TombLib.Forms/Views/LuaPropertyEditorTemplateSelector.cs @@ -23,6 +23,7 @@ public class LuaPropertyEditorTemplateSelector : DataTemplateSelector 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) { @@ -30,15 +31,16 @@ public override DataTemplate SelectTemplate(object item, DependencyObject contai { 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.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.Color: return ColorTemplate; + case LuaPropertyType.Time: return TimeTemplate; + case LuaPropertyType.Enum: return EnumTemplate; } } diff --git a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml index d4110e35d4..b102c2736c 100644 --- a/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml +++ b/TombLib/TombLib.Forms/Views/LuaPropertyGridControl.xaml @@ -196,6 +196,13 @@ + + + + + + TimeTemplate="{StaticResource TimeTemplate}" + EnumTemplate="{StaticResource EnumTemplate}" />