Skip to content
Merged
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
86 changes: 86 additions & 0 deletions .github/agents/theasseteditor.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: TheAssetEditor Coding Agent
description: "Use when working on TheAssetEditor C#/.NET/WPF codebase: AssetEditor app, Editors modules, Shared libraries, GameWorld, tests, dependency injection, XAML, and build/test validation."
tools: [read, search, edit, execute, todo]
user-invocable: true
---
You are a specialized coding agent for TheAssetEditor.

Your goal is to produce safe, minimal, verifiable changes that align with existing architecture and conventions.

## Repository Context
- This is a large multi-project .NET solution centered on `AssetEditor.sln`.
- Main app is WPF (`AssetEditor/AssetEditor.csproj`) targeting `net10.0-windows`, with `LangVersion=preview` and nullable enabled.
- Architecture is modular:
- `AssetEditor/` for shell app and composition.
- `Editors/` for feature editors.
- `Shared/` for shared core, UI, formats, and utilities.
- `GameWorld/` for rendering/3D systems.
- `Testing/` for test projects.
- Dependency injection is heavily used (`Microsoft.Extensions.DependencyInjection`) with scoped/transient/singleton lifetimes.

## Operating Principles
1. Preserve behavior unless the task explicitly asks for functional changes.
2. Prefer small, localized edits over broad refactors.
3. Match existing naming, style, and file organization.
4. Do not introduce new frameworks or patterns unless clearly justified by existing usage.
5. Keep WPF/XAML changes consistent with existing localization and binding patterns.
6. Prefer the UiCommand pattern when linking UI actions to functionality.
7. Optimize code for unit testing and avoid designs that require implementing fake classes.
8. Any change in `Shared.Core` must include corresponding unit tests in the same work item.
9. Any change in `Shared.GameFormats` requires explicit user permission before implementation.
10. Do not modify `RenderEngineComponent.cs` unless the user explicitly requests it.
11. For 3D world rendering or draw-loop integrations, add `RenderItems` instead of modifying the core render engine loop.

## C# and Style Rules
- Follow `.editorconfig` as source of truth.
- Use spaces, 4-space indentation in C# files.
- Keep `nullable` expectations intact; avoid broad nullable annotation churn.
- Naming conventions to preserve:
- Instance fields: `_camelCase`
- Static fields: `s_camelCase`
- Members/types: `PascalCase`
- Keep `using` directives sorted with `System.*` first.
- Prefer minimal comments; add comments only for non-obvious logic.

## WPF/XAML Rules
- Keep existing MVVM/data-binding patterns.
- Prefer UiCommand and IUiCommandFactory over direct UI event-handler logic when wiring functionality from UI interactions.
- Reuse Shared UI localization conventions already in repo.
- For cross-project localization namespaces, follow existing `Shared.Ui` assembly-qualified `xmlns` patterns.
- Avoid visual or resource-key churn outside the task scope.

## Dependency Injection Rules
- Register services in existing composition roots and preserve lifetimes unless a change requires otherwise.
- Prefer existing abstractions/interfaces when adding dependencies.
- Do not duplicate registrations unless the pattern already intentionally does so.

## Testing and Validation
Always validate with the smallest relevant scope first:
1. Build changed project(s):
- `dotnet build .\AssetEditor\AssetEditor.csproj -nologo`
2. Run targeted tests for affected area (project/file-level where possible).
3. If needed for confidence, run broader validation:
- `dotnet test .\AssetEditor.sln --configuration Release --no-restore --verbosity normal`

When tests are mixed-framework, preserve existing framework choice (NUnit and MSTest both exist in this repository).

## Testability Design Rules
- Design for unit testing by favoring small, focused classes with explicit dependencies and deterministic behavior.
- Avoid coupling business logic to UI framework types where possible; keep logic behind testable seams.
- Prefer existing abstractions and dependency injection instead of creating new layers purely for mocking.
- Avoid designs that require implementing fake classes to test core behavior.

## Safety Checklist Before Finalizing
- Change is limited to requested scope.
- No unrelated formatting or file movement.
- Build/test command results are checked.
- Public APIs are unchanged unless requested.
- XAML namespace/localization conventions remain valid.

## Response Format
When reporting back:
1. Briefly state what changed.
2. List exact files touched.
3. Summarize validation performed (build/tests and outcome).
4. Call out any risks, assumptions, or follow-up options.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"ForEach-Object": true
}
}
2 changes: 2 additions & 0 deletions AssetEditor/DependencyInjectionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public override void Register(IServiceCollection serviceCollection)
serviceCollection.AddTransient<OpenUpdaterWindowCommand>();
serviceCollection.AddTransient<OpenWebpageCommand>();
serviceCollection.AddTransient<PrintScopesCommand>();
serviceCollection.AddTransient<PrintTrackedGraphicsResourcesCommand>();
serviceCollection.AddTransient<OpenEditorCommand>();
serviceCollection.AddTransient<TogglePackFileExplorerCommand>();

Expand All @@ -43,6 +44,7 @@ public override void Register(IServiceCollection serviceCollection)

serviceCollection.AddScoped<IExceptionInformationProvider, CurrentEditorExceptionInfoProvider>();


RegisterAllAsInterface<IDeveloperConfiguration>(serviceCollection, ServiceLifetime.Transient);
}
}
Expand Down
6 changes: 4 additions & 2 deletions AssetEditor/Language_Cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@
"MenuBar.Reports.TouchedFiles.PrintFiles": "打印文件",
"MenuBar.Reports.TouchedFiles.ExtractToPack": "导出到 Pack",
"MenuBar.Reports.TouchedFiles.StopRecorder": "停止记录",
"MenuBar.Reports.DebugClearConsole": "调试 - 清空控制台",
"MenuBar.Reports.DebugPrintScopes": "调试 - 打印作用域",
"MenuBar.Debug": "调试",
"MenuBar.Debug.ClearConsole": "清空控制台",
"MenuBar.Debug.PrintScopes": "打印作用域",
"MenuBar.Debug.PrintTrackedGraphicsResources": "打印已跟踪图形资源",
"MenuBar.Help": "帮助",
"MenuBar.Help.AssetEditorHelp": "AssetEditor 帮助",
"MenuBar.Help.ModdingWiki": "Mod 百科",
Expand Down
6 changes: 4 additions & 2 deletions AssetEditor/Language_En.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@
"MenuBar.Reports.TouchedFiles.PrintFiles": "Print Files",
"MenuBar.Reports.TouchedFiles.ExtractToPack": "Extract to Pack",
"MenuBar.Reports.TouchedFiles.StopRecorder": "Stop Recorder",
"MenuBar.Reports.DebugClearConsole": "Debug - Clear Console",
"MenuBar.Reports.DebugPrintScopes": "Debug - Print Scopes",
"MenuBar.Debug": "Debug",
"MenuBar.Debug.ClearConsole": "Clear Console",
"MenuBar.Debug.PrintScopes": "Print Scopes",
"MenuBar.Debug.PrintTrackedGraphicsResources": "Print Tracked Graphics Resources",
"MenuBar.Help": "Help",
"MenuBar.Help.AssetEditorHelp": "AssetEditor Help",
"MenuBar.Help.ModdingWiki": "Modding Wiki",
Expand Down
61 changes: 61 additions & 0 deletions AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameWorld.Core.Services;
using Shared.Core.DependencyInjection;
using Shared.Core.ErrorHandling.Exceptions;
using Shared.Core.ToolCreation;

namespace AssetEditor.Services
{
internal class GraphicsResourceExceptionInfoProvider : IExceptionInformationProvider
{
private readonly IEditorManager _editorManager;
private readonly IScopeRepository _scopeRepository;

public GraphicsResourceExceptionInfoProvider(IEditorManager editorManager, IScopeRepository scopeRepository)
{
_editorManager = editorManager;
_scopeRepository = scopeRepository;
}

public void HydrateExcetion(ExceptionInformation exceptionInformation)
{
try
{
var allEditors = _editorManager.GetAllEditors();
var currentEditorIndex = _editorManager.GetCurrentEditor();
var hasCurrentEditor = currentEditorIndex >= 0 && currentEditorIndex < allEditors.Count;
var currentEditor = hasCurrentEditor ? allEditors[currentEditorIndex] : null;

var editorHandles = _scopeRepository.GetEditorHandles();

foreach (var editor in editorHandles)
{
var isCurrentScope = currentEditor != null && ReferenceEquals(editor, currentEditor);

try
{
var creator = _scopeRepository.GetRequiredService<IGraphicsResourceCreator>(editor);
var records = creator.Records;
var resourceLines = records
.Select(x => $"id={x.ResourceId} | {x.ResourceType} | owner={x.ScopeOwner} | source={x.SourceFile}:{x.SourceLine}::{x.SourceMember}")
.ToList();

exceptionInformation.GraphicsResourceScopes.Add(
new GraphicsResourceScopeInfo(creator.ScopeOwner, isCurrentScope, records.Count, resourceLines));
}
catch
{
exceptionInformation.GraphicsResourceScopes.Add(
new GraphicsResourceScopeInfo($"{editor.DisplayName} ({editor.GetType().Name})", isCurrentScope, 0, new List<string>()));
}
}
}
catch (Exception e)
{
exceptionInformation.CurrentEditorGraphicsResourceInfoError = e.Message;
}
}
}
}
74 changes: 74 additions & 0 deletions AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Text;
using GameWorld.Core.Services;
using Serilog;
using Shared.Core.DependencyInjection;
using Shared.Core.ErrorHandling;
using Shared.Core.Events;
using Shared.Core.ToolCreation;

namespace AssetEditor.UiCommands
{
public class PrintTrackedGraphicsResourcesCommand : IUiCommand
{
private readonly ILogger _logger = Logging.Create<PrintTrackedGraphicsResourcesCommand>();
private readonly IScopeRepository _scopeRepository;
private readonly IEditorManager _editorManager;

public PrintTrackedGraphicsResourcesCommand(IScopeRepository scopeRepository, IEditorManager editorManager)
{
_scopeRepository = scopeRepository;
_editorManager = editorManager;
}

public void Execute()
{
var builder = new StringBuilder();
var allEditors = _editorManager.GetAllEditors();
var currentEditorIndex = _editorManager.GetCurrentEditor();
var hasCurrentEditor = currentEditorIndex >= 0 && currentEditorIndex < allEditors.Count;
var currentEditor = hasCurrentEditor ? allEditors[currentEditorIndex] : null;

var editorHandles = _scopeRepository.GetEditorHandles();

builder.AppendLine();
builder.AppendLine("=== Graphics Resource Tracker Dump ===");
builder.AppendLine($"Scopes: {editorHandles.Count}");

var totalTrackedResources = 0;
foreach (var editor in editorHandles)
{
var isCurrentScope = currentEditor != null && ReferenceEquals(editor, currentEditor);
var marker = isCurrentScope ? " [CURRENT]" : string.Empty;
builder.AppendLine($"Scope: {editor.DisplayName} ({editor.GetType().Name}){marker}");

try
{
var creator = _scopeRepository.GetRequiredService<IGraphicsResourceCreator>(editor);
var records = creator.Records;
builder.AppendLine($" Items in scope: {records.Count}");
if (records.Count == 0)
{
builder.AppendLine(" (no tracked graphics resources)");
continue;
}

foreach (var record in records)
{
totalTrackedResources++;
builder.AppendLine($" - id={record.ResourceId} | {record.ResourceType} | owner={record.ScopeOwner} | source={record.SourceFile}:{record.SourceLine}::{record.SourceMember}");
}
}
catch
{
builder.AppendLine(" Items in scope: 0");
builder.AppendLine(" (no graphics resource tracker in this scope)");
}
}

builder.AppendLine($"Total tracked items across all scopes: {totalTrackedResources}");
builder.AppendLine("=== End Graphics Resource Tracker Dump ===");
_logger.Here().Information(builder.ToString());
}
}
}
3 changes: 3 additions & 0 deletions AssetEditor/ViewModels/MenuBarViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ [RelayCommand] private void CreateNewPackFile()
[RelayCommand] private void TouchedFileRecorderExtract() => _touchedFilesRecorder.ExtractFilesToPack(@"c:\temp\extractedPack.pack");
[RelayCommand] private void TouchedFileRecorderStop() => _touchedFilesRecorder.Stop();

public bool IsDebuggerAttached => Debugger.IsAttached;

[RelayCommand] private void ClearConsole() => Console.Clear();
[RelayCommand] private void PrintScope() => _uiCommandFactory.Create<PrintScopesCommand>().Execute();
[RelayCommand] private void PrintTrackedGraphicsResources() => _uiCommandFactory.Create<PrintTrackedGraphicsResourcesCommand>().Execute();
[RelayCommand] private void Search() => _uiCommandFactory.Create<DeepSearchCommand>().Execute();
[RelayCommand] private void OpenAttilaPacks() => _uiCommandFactory.Create<OpenGamePackCommand>().Execute(GameTypeEnum.Attila);
[RelayCommand] private void OpenRomeRemasteredPacks() => _uiCommandFactory.Create<OpenGamePackCommand>().Execute(GameTypeEnum.RomeRemastered);
Expand Down
14 changes: 11 additions & 3 deletions AssetEditor/Views/MenuBarView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="800">

<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
</UserControl.Resources>

<DockPanel>
<Menu VerticalAlignment="Center"
DockPanel.Dock="Left"
Expand Down Expand Up @@ -112,9 +116,13 @@
<MenuItem Header="{loc:Loc MenuBar.Reports.TouchedFiles.ExtractToPack}" Command="{Binding MenuBar.TouchedFileRecorderExtractCommand }"/>
<MenuItem Header="{loc:Loc MenuBar.Reports.TouchedFiles.StopRecorder}" Command="{Binding MenuBar.TouchedFileRecorderStopCommand }"/>
</MenuItem>
<Separator/>
<MenuItem Header="{loc:Loc MenuBar.Reports.DebugClearConsole}" Command="{Binding MenuBar.ClearConsoleCommand}" />
<MenuItem Header="{loc:Loc MenuBar.Reports.DebugPrintScopes}" Command="{Binding MenuBar.PrintScopeCommand}" />
</MenuItem>

<MenuItem Header="{loc:Loc MenuBar.Debug}" BorderThickness="1.5" WindowChrome.IsHitTestVisibleInChrome="True"
Visibility="{Binding MenuBar.IsDebuggerAttached, Converter={StaticResource BoolToVisibility}}">
<MenuItem Header="{loc:Loc MenuBar.Debug.ClearConsole}" Command="{Binding MenuBar.ClearConsoleCommand}" />
<MenuItem Header="{loc:Loc MenuBar.Debug.PrintScopes}" Command="{Binding MenuBar.PrintScopeCommand}" />
<MenuItem Header="{loc:Loc MenuBar.Debug.PrintTrackedGraphicsResources}" Command="{Binding MenuBar.PrintTrackedGraphicsResourcesCommand}" />
</MenuItem>

<MenuItem Header="{loc:Loc MenuBar.Help}" BorderThickness="1.5" WindowChrome.IsHitTestVisibleInChrome="True">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewmodel="clr-namespace:KitbasherEditor.ViewModels.MenuBarViews"
xmlns:menusystem="clr-namespace:Shared.Ui.Common.MenuSystem;assembly=Shared.Ui"
mc:Ignorable="d" Loaded="UserControl_Loaded" >
mc:Ignorable="d" Loaded="UserControl_Loaded" Unloaded="UserControl_Unloaded" >
<UserControl.Resources>

<Style x:Key="VerticalSeparatorStyle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,41 @@ private void UserControl_Loaded(object sender, RoutedEventArgs e)
var window = Window.GetWindow(this);
if (window != null)
{
window.KeyUp += HandleKeyPress;
window.KeyUp += HandleKeyUp;
window.KeyDown += HandleKeyDown;
}
}

private void HandleKeyPress(object sender, KeyEventArgs e)
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is TextBox)
var window = Window.GetWindow(this);
if (window != null)
{
e.Handled = true;
return;
window.KeyUp -= HandleKeyUp;
window.KeyDown -= HandleKeyDown;
}
}

private void HandleKeyUp(object sender, KeyEventArgs e)
{
if (e.OriginalSource is TextBox && Keyboard.Modifiers == ModifierKeys.None)
return;

if (DataContext is IKeyboardHandler keyboardHandler)
{
var res = keyboardHandler.OnKeyReleased(e.Key, e.SystemKey, Keyboard.Modifiers);
if (res)
e.Handled = true;
}
keyboardHandler.OnKeyReleased(e.Key, e.SystemKey, Keyboard.Modifiers);
}

private void HandleKeyDown(object sender, KeyEventArgs e)
{
if (e.OriginalSource is TextBox && Keyboard.Modifiers == ModifierKeys.None)
return;

if (DataContext is IKeyboardHandler keyboardHandler)
{
keyboardHandler.OnKeyDown(e.Key, e.SystemKey, Keyboard.Modifiers);
var res = keyboardHandler.OnKeyReleased(e.Key, e.SystemKey, Keyboard.Modifiers);
if (res)
e.Handled = true;
}
}
}
Expand Down
Loading
Loading