diff --git a/.github/agents/theasseteditor.agent.md b/.github/agents/theasseteditor.agent.md new file mode 100644 index 000000000..b6a4f3c75 --- /dev/null +++ b/.github/agents/theasseteditor.agent.md @@ -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. diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..9a85d1da1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "ForEach-Object": true + } +} \ No newline at end of file diff --git a/AssetEditor/DependencyInjectionContainer.cs b/AssetEditor/DependencyInjectionContainer.cs index 9633f055f..bfcff145b 100644 --- a/AssetEditor/DependencyInjectionContainer.cs +++ b/AssetEditor/DependencyInjectionContainer.cs @@ -28,6 +28,7 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); @@ -43,6 +44,7 @@ public override void Register(IServiceCollection serviceCollection) serviceCollection.AddScoped(); + RegisterAllAsInterface(serviceCollection, ServiceLifetime.Transient); } } diff --git a/AssetEditor/Language_Cn.json b/AssetEditor/Language_Cn.json index 4bd6e633e..ee79974ce 100644 --- a/AssetEditor/Language_Cn.json +++ b/AssetEditor/Language_Cn.json @@ -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 百科", diff --git a/AssetEditor/Language_En.json b/AssetEditor/Language_En.json index e39749eaf..633f80823 100644 --- a/AssetEditor/Language_En.json +++ b/AssetEditor/Language_En.json @@ -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", diff --git a/AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs b/AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs new file mode 100644 index 000000000..3cdeac5d0 --- /dev/null +++ b/AssetEditor/Services/GraphicsResourceExceptionInfoProvider.cs @@ -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(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())); + } + } + } + catch (Exception e) + { + exceptionInformation.CurrentEditorGraphicsResourceInfoError = e.Message; + } + } + } +} diff --git a/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs b/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs new file mode 100644 index 000000000..32a278456 --- /dev/null +++ b/AssetEditor/UiCommands/PrintTrackedGraphicsResourcesCommand.cs @@ -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(); + 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(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()); + } + } +} diff --git a/AssetEditor/ViewModels/MenuBarViewModel.cs b/AssetEditor/ViewModels/MenuBarViewModel.cs index 81e6854a6..f9fc92325 100644 --- a/AssetEditor/ViewModels/MenuBarViewModel.cs +++ b/AssetEditor/ViewModels/MenuBarViewModel.cs @@ -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().Execute(); + [RelayCommand] private void PrintTrackedGraphicsResources() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void Search() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void OpenAttilaPacks() => _uiCommandFactory.Create().Execute(GameTypeEnum.Attila); [RelayCommand] private void OpenRomeRemasteredPacks() => _uiCommandFactory.Create().Execute(GameTypeEnum.RomeRemastered); diff --git a/AssetEditor/Views/MenuBarView.xaml b/AssetEditor/Views/MenuBarView.xaml index 883b81eed..55ca66064 100644 --- a/AssetEditor/Views/MenuBarView.xaml +++ b/AssetEditor/Views/MenuBarView.xaml @@ -9,6 +9,10 @@ mc:Ignorable="d" d:DesignHeight="30" d:DesignWidth="800"> + + + + - - - + + + + + + diff --git a/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml b/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml index ae9cdc800..d96b8bf08 100644 --- a/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml +++ b/Editors/Kitbashing/KitbasherEditor/Core/MenuBarViews/MenuBarView.xaml @@ -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" >