Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
## General Project Information

- Language: **C# targeting .NET 8** (desktop application).
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This states the project targets .NET 8, but multiple .csproj files in this PR target net6.0-windows. Update AGENTS.md to match the repository’s actual target framework(s) to avoid misleading contributors/tooling.

Suggested change
- Language: **C# targeting .NET 8** (desktop application).
- Language: **C# targeting .NET 6 (`net6.0-windows`)** (desktop application).

Copilot uses AI. Check for mistakes.
- This is a level editor suite for a family of 3D game engines used in the classic Tomb Raider series.
- Level formats are grid-based, room-based, and portal-based.
- A room is a spatial container for level geometry and game entities.
- Rooms are connected by vertical or horizontal portals, strictly aligned with grid sectors.
- Portals may be visual (`RoomConnectionInfo.VisualType`) or traversable (`RoomConnectionInfo.TraversableType`).
- One grid sector consists of 1024 units, which roughly equals 2 meters in real-world coordinates.

## General Guidelines

- Files must use Windows line endings. Only standard ASCII symbols are allowed; do not use Unicode symbols.
- `using` directives are grouped and sorted as follows: `DarkUI` namespaces first, then `System` namespaces, followed by third-party and local namespaces.
- Namespace declarations and type definitions should place the opening brace on a new line.
- Prefer grouping all feature-related functionality within a self-contained module or modules. Avoid creating large code blocks over 10–15 lines in existing modules; instead, offload code to helper functions.
- Avoid duplicating and copypasting code. Implement helper methods instead, whenever similar code is used within a given module, class or feature scope.

## Formatting

- **Indentation** is four spaces; tabs are not used.

- **Braces**:
- Always use braces for multi-statement blocks.
- Do not use braces for single-statement blocks, unless they are within multiple `else if` conditions where surrounding statements are multi-line.

- Opening curly brace `{` for structures, classes and methods should be on the next line, not on the same line:

```csharp
public class Foo
{
public void Bar()
{
if (condition)
{
...
}
}
}
```

- Anonymous delegates and lambdas should keep the brace on the same line:
`delegate () { ... }` or `() => { ... }`.

- **Line breaks and spacing**:
- A blank line separates logically distinct groups of members (fields, constructors, public methods, private helpers, etc.).
- Spaces around binary operators (`=`, `+`, `==`, etc.) and after commas.
- A single space follows keyword `if`/`for`/`while` before the opening parenthesis.
- Expressions may be broken into multiple lines and aligned with the previous line's indentation level to improve readability.
- However, chained LINQ method calls, lambdas or function/method arguments should not be broken into multiple lines, unless they reach more than 150 symbols in length.

- Do not collapse early exits or single-statement conditions into a single line:

Bad example:
```csharp
if (condition) return;
```
Do this instead:
```csharp
if (condition)
return;
```

## Naming

- **PascalCase** for public types, methods, constants, properties and events.
- **camelCase** for private fields and local variables. Private fields should start with an underscore (`_editor`, `_primaryControlFocused`). Local variables should not start with an underscore.
- Constants and `static readonly` fields use PascalCase rather than ALL_CAPS.
- Enum members use PascalCase.
- Interfaces are prefixed with `I` and use PascalCase (`IScaleable`).
- Methods and variables should use clear, descriptive names and generally avoid Hungarian notation. Avoid using short non-descriptive names, such as `s2`, `rwh`, `fmp`, unless underlying meaning is brief (e.g. X coordinate is `x`, counter is `i`).
- Class method and field names should not repeat words from a class name itself (e.g. `ObjectBrushHelper.BeginObjectBrushStroke` is a bad name, but `ObjectBrushHelper.BeginStroke` is a good name).

## Members and Access

- Fields are generally declared as `public` or `private readonly` depending on usage; expose state via properties where appropriate.
- `var` type should be preferred where possible, when the right-hand type is evident from the initializer.
- Explicit typing should be only used when it is required by logic or compiler, or when type name is shorter than 6 symbols (e.g. `int`, `bool`, `float`).
- For floating-point numbers, always use `f` postfix and decimal, even if value is not fractional (e.g. `2.0f`).

## Control Flow and Syntax

- Avoid excessive condition nesting and use early exits / breaks where possible.
- LINQ and lambda expressions are used for collections (`FirstOrDefault`, `Where`, `Any`, etc.).
- Exception and error handling is done with `try`/`catch`, and caught exceptions are logged with [NLog](https://nlog-project.org/) where appropriate.
- Warnings must also be logged by NLog, if cause for the incorrect behaviour is user action.

## Comments

- When comments appear they are single-line `//`. Block comments (`/* ... */`) are rare.
- Comments are sparse. Code relies on meaningful names rather than inline documentation.
- Do not use `<summary>` if surrounding code and/or module isn't already using it. Only add `<summary>` for non-private methods with high complexity.
- If module or function implements complex functionality, a brief description (2-3 lines) may be added in front of it, separated by a blank line from the function body.
- All descriptive comments should end with a full stop (`.`).

## Code Grouping

- Large methods should group related actions together, separated by blank lines.
- Constants and static helpers that are used several times should appear at the top of a class.
- Constants that are used only within a scope of a method, should be declared within this method.
- One-liner lambdas may be grouped together, if they share similar meaning or functionality.

## User Interface Implementation

- For WinForms-based workflows, maintain the existing Visual Studio module pair for each control or unit: `.cs` and `.Designer.cs`.
- For existing WinForms-based `DarkUI` controls and containers, prefer to use existing WinForms-based `DarkUI` controls.
- For new controls and containers with complex logic, or where WinForms may not perform fast enough, prefer `DarkUI.WPF` framework. Use `GeometryIOSettingsWindow` as a reference.
- Use `CommunityToolkit` functionality where possible.

## Performance

- For 3D rendering controls, prefer more performant approaches and locally cache frequently used data within the function scope whenever possible.
- Avoid scenarios where bulk data updates may cause event floods, as the project relies heavily on event subscriptions across multiple controls and sub-controls.
- Use `Parallel` for bulk operations to maximize performance. Avoid using it in thread-unsafe contexts or when operating on serial data sets.
15 changes: 15 additions & 0 deletions DarkUI/DarkUI.WPF/Converters/InverseBoolToVisibilityConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace DarkUI.WPF.Converters;

public class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is bool b && b ? Visibility.Collapsed : Visibility.Visible;

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
130 changes: 130 additions & 0 deletions DarkUI/DarkUI.WPF/CustomControls/SpacedGrid.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System;
using System.Windows;
using System.Windows.Controls;

namespace DarkUI.WPF.CustomControls;

/// <summary>
/// A Grid that supports RowSpacing and ColumnSpacing between rows and columns
/// without injecting dummy rows or columns.
/// </summary>
public class SpacedGrid : Grid
{
public static readonly DependencyProperty RowSpacingProperty =
DependencyProperty.Register(
nameof(RowSpacing),
typeof(double),
typeof(SpacedGrid),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

public static readonly DependencyProperty ColumnSpacingProperty =
DependencyProperty.Register(
nameof(ColumnSpacing),
typeof(double),
typeof(SpacedGrid),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

/// <summary>
/// Gets or sets the vertical spacing between rows.
/// </summary>
public double RowSpacing
{
get => (double)GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}

/// <summary>
/// Gets or sets the horizontal spacing between columns.
/// </summary>
public double ColumnSpacing
{
get => (double)GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}

protected override Size MeasureOverride(Size constraint)
{
double totalRowSpacing = GetTotalRowSpacing();
double totalColSpacing = GetTotalColumnSpacing();

var reducedConstraint = new Size(
double.IsInfinity(constraint.Width) ? constraint.Width : Math.Max(0, constraint.Width - totalColSpacing),
double.IsInfinity(constraint.Height) ? constraint.Height : Math.Max(0, constraint.Height - totalRowSpacing));

var baseSize = base.MeasureOverride(reducedConstraint);

return new Size(baseSize.Width + totalColSpacing, baseSize.Height + totalRowSpacing);
}

protected override Size ArrangeOverride(Size arrangeSize)
{
double rowSpacing = RowSpacing;
double colSpacing = ColumnSpacing;
double totalRowSpacing = GetTotalRowSpacing();
double totalColSpacing = GetTotalColumnSpacing();

// Let the base Grid arrange children within a reduced area.
var reducedSize = new Size(
Math.Max(0, arrangeSize.Width - totalColSpacing),
Math.Max(0, arrangeSize.Height - totalRowSpacing));

base.ArrangeOverride(reducedSize);

int rowCount = Math.Max(1, RowDefinitions.Count);
int colCount = Math.Max(1, ColumnDefinitions.Count);

// Build cumulative row offsets from ActualHeight after base layout.
double[] rowStarts = new double[rowCount + 1];

for (int i = 0; i < rowCount; i++)
{
double height = RowDefinitions.Count > 0 ? RowDefinitions[i].ActualHeight : reducedSize.Height;
rowStarts[i + 1] = rowStarts[i] + height;
}

// Build cumulative column offsets from ActualWidth after base layout.
double[] colStarts = new double[colCount + 1];

for (int i = 0; i < colCount; i++)
{
double width = ColumnDefinitions.Count > 0 ? ColumnDefinitions[i].ActualWidth : reducedSize.Width;
colStarts[i + 1] = colStarts[i] + width;
}

// Re-arrange each child, injecting spacing offsets.
foreach (UIElement child in InternalChildren)
{
if (child == null || child.Visibility == Visibility.Collapsed)
continue;

int row = Math.Min(GetRow(child), rowCount - 1);
int col = Math.Min(GetColumn(child), colCount - 1);
int rowSpan = Math.Min(GetRowSpan(child), rowCount - row);
int colSpan = Math.Min(GetColumnSpan(child), colCount - col);

// Position = base offset + cumulative spacing for preceding rows/columns.
double x = colStarts[col] + col * colSpacing;
double y = rowStarts[row] + row * rowSpacing;

// Size = base span size + spacing gaps within the span.
double width = colStarts[col + colSpan] - colStarts[col] + (colSpan - 1) * colSpacing;
double height = rowStarts[row + rowSpan] - rowStarts[row] + (rowSpan - 1) * rowSpacing;

child.Arrange(new Rect(x, y, Math.Max(0, width), Math.Max(0, height)));
}

return arrangeSize;
}

private double GetTotalRowSpacing()
{
int gaps = Math.Max(0, (RowDefinitions.Count > 0 ? RowDefinitions.Count : 1) - 1);
return gaps * RowSpacing;
}

private double GetTotalColumnSpacing()
{
int gaps = Math.Max(0, (ColumnDefinitions.Count > 0 ? ColumnDefinitions.Count : 1) - 1);
return gaps * ColumnSpacing;
}
}
8 changes: 8 additions & 0 deletions DarkUI/DarkUI.WPF/Extensions/DependencyObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public static class DependencyObjectExtensions
return ancestor as T;
}

public static T? FindVisualAncestorOrSelf<T>(this DependencyObject dependencyObject) where T : class
{
if (dependencyObject is T self)
return self;

return dependencyObject.FindVisualAncestor<T>();
}

public static T? FindLogicalAncestor<T>(this DependencyObject dependencyObject) where T : class
{
DependencyObject ancestor = dependencyObject;
Expand Down
1 change: 1 addition & 0 deletions DarkUI/DarkUI.WPF/Generic.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<ResourceDictionary Source="Styles/RadioButton.xaml" />
<ResourceDictionary Source="Styles/ScrollViewer.xaml" />
<ResourceDictionary Source="Styles/SearchableComboBox.xaml" />
<ResourceDictionary Source="Styles/Slider.xaml" />
<ResourceDictionary Source="Styles/StatusBar.xaml" />
<ResourceDictionary Source="Styles/TabControl.xaml" />
<ResourceDictionary Source="Styles/TextBlock.xaml" />
Expand Down
70 changes: 70 additions & 0 deletions DarkUI/DarkUI.WPF/Styles/Slider.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DarkUI.WPF">

<Style x:Key="SliderRepeatButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Focusable" Value="False" />

<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="SliderThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Border
Width="10"
Height="16"
Background="{DynamicResource Brush_Background_High}"
BorderBrush="{DynamicResource Brush_Border}"
BorderThickness="1"
CornerRadius="2" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style TargetType="{x:Type Slider}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="{x:Static local:Defaults.SnapsToDevicePixels}" />
<Setter Property="UseLayoutRounding" Value="{x:Static local:Defaults.UseLayoutRounding}" />

<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Slider}">
<Grid VerticalAlignment="Center">
<Border
Height="4"
Margin="5,0"
Background="{DynamicResource Brush_Background_Low}"
BorderBrush="{DynamicResource Brush_Border}"
BorderThickness="1"
CornerRadius="2" />

<Track x:Name="PART_Track">
<Track.DecreaseRepeatButton>
<RepeatButton Command="Slider.DecreaseLarge" Style="{StaticResource SliderRepeatButtonStyle}" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource SliderThumbStyle}" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="Slider.IncreaseLarge" Style="{StaticResource SliderRepeatButtonStyle}" />
</Track.IncreaseRepeatButton>
</Track>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
10 changes: 8 additions & 2 deletions Installer/Changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,35 @@ Tomb Editor:
* Added support for triangular geometry in TR1X and TR2X levels.
* Added a texture depth option (8/16/32-bit) for TR1X and TR2X levels.
* Added support for TR1X and TR2X levels to use more sound slots (maximum 1000 samples in total per level).
* Added support for monkeyswing in TR1X and TR2X levels.
* Added support for no caustics room flag in TEN.
* Changed TR2X levels to embed sound effects rather than using main.sfx.
* Fixed node parameter corruption after changing node background color.
* Fixed unintended room deletion in 3D mode when no objects are selected.
* Fixed enemy monkeyswing pathfinding being added to TR3 levels.

WadTool:
* Added indication of missing external texture file.
* Automatically correct hair mesh connections when importing TR4 outfits to TEN wad.
* Rearranged animation editor UI in preparation for animation blending feature.
* Fixed gizmo behaviour in animation editor.
* Fixed Y/P numerical rotation controls being reversed.
* Fixed misplaced backholster meshes for classic engines.
* Fixed external textures not loading when wad was moved to another directory.
* Fixed exception while loading wads with material xml files placed in the same folder as wad.
* Fixed exception while exporting meshes with degenerate textures.

TombIDE:
* Updated TR1X and TR2X presets to TRX 1.2.2.
* Updated PLAY.exe to fix TEN console window not showing in `-debug` mode.
* Updated "Ready to Play" archive creation dialog.
* Updated TR1X and TR2X presets to TRX 1.3.1.
* Updated PLAY.exe to fix TEN console window not showing in `-debug` mode.
* Updated FLEP.

TEN nodes:
* Added nodes to check the color of a moveable or static mesh.
* Added nodes to show or hide interaction highlight for a moveable.
* Added nodes to use inventory item, set inventory item count and to reset inventory to default.
* Added a node to clear all input keys.
* Fixed crash with keypad nodes on certain system configurations.

Version 1.10.1
Expand Down
Loading
Loading