diff --git a/.gitignore b/.gitignore index 9d793d1624..6110f9a0e7 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ fastlane/report.xml fastlane/screenshots *.user project.lock.json + +# LLM +.claude/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..742c6327e0 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "stride-game-runtime": { + "type": "sse", + "url": "http://localhost:5272/sse" + }, + "stride-game-studio": { + "type": "sse", + "url": "http://localhost:5271/sse" + } + } +} diff --git a/MCP_IMPLEMENTATION_PLAN.md b/MCP_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000000..9df746be93 --- /dev/null +++ b/MCP_IMPLEMENTATION_PLAN.md @@ -0,0 +1,848 @@ +# Stride3D MCP Server - Complete Implementation Plan + +## Project Vision + +Build a Model Context Protocol (MCP) server integrated into Stride3D Game Studio that enables full programmatic control of the editor through LLM agents. The server will support multiple concurrent GameStudio instances and provide comprehensive read/write access to all editor state. + +## Scope & Capabilities + +### 1. State Reading +- **Assets**: Enumerate, inspect metadata, read content +- **Scenes**: List open scenes, read scene hierarchy +- **Entities**: Inspect entity tree, read transforms, read all components +- **Components**: Read all component properties and values +- **Projects**: Read project structure, dependencies, build configuration +- **UI/Sprites**: Read UI pages, sprite sheet definitions + +### 2. Navigation & Visualization +- **Scene Navigation**: Open scenes, focus on entities, navigate hierarchy +- **Asset Navigation**: Open folders, filter assets, search +- **Viewport Control**: Pan, zoom, rotate camera +- **Screenshot Capture**: Capture viewports, UI panels, sprite sheets +- **Selection**: Select entities, components, assets + +### 3. Modification +- **Entity Operations**: Create, delete, move, parent/unparent +- **Component Operations**: Add, remove, modify component properties +- **Transform Operations**: Move, rotate, scale entities +- **Asset Operations**: Import, delete, rename assets +- **UI Editing**: Modify UI pages, add/remove elements +- **Sprite Sheet Editing**: Edit sprite definitions, adjust frames + +### 4. Lifecycle Management +- **Studio Control**: Launch, close, restart GameStudio +- **Project Management**: Open, create, close projects +- **Build Operations**: Build, clean, package projects +- **Multi-Instance**: Support multiple GameStudio instances simultaneously + +## Architecture Overview + +``` +┌────────────────────────────────────────────────────────────┐ +│ MCP Client (Claude) │ +└────────────────┬───────────────────────────────────────────┘ + │ JSON-RPC over stdio + ▼ +┌────────────────────────────────────────────────────────────┐ +│ MCP Server Launcher Process │ +│ - Manages multiple GameStudio instances │ +│ - Routes requests to appropriate instance │ +│ - Handles instance lifecycle │ +└────────┬───────────────────────────┬───────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────┐ +│ GameStudio #1 │ │ GameStudio #2 │ +│ ┌───────────────┐ │ │ ┌───────────────┐ │ +│ │ MCP Plugin │ │ │ │ MCP Plugin │ │ +│ │ - Tools │ │ │ │ - Tools │ │ +│ │ - Services │ │ │ │ - Services │ │ +│ │ - IPC Client │◄─┼────┼─►│ - IPC Client │ │ +│ └───────────────┘ │ │ └───────────────┘ │ +│ ┌───────────────┐ │ │ ┌───────────────┐ │ +│ │ GameStudio VM │ │ │ │ GameStudio VM │ │ +│ │ - Session │ │ │ │ - Session │ │ +│ │ - Scenes │ │ │ │ - Scenes │ │ +│ │ - Assets │ │ │ │ - Assets │ │ +│ └───────────────┘ │ │ └───────────────┘ │ +└─────────────────────┘ └─────────────────────┘ +``` + +## Project Structure + +``` +sources/ +├── editor/ +│ ├── Stride.Assets.Presentation.MCP/ # NEW: MCP Plugin for GameStudio +│ │ ├── Stride.Assets.Presentation.MCP.csproj +│ │ ├── Plugin/ +│ │ │ ├── McpPlugin.cs # Plugin entry point +│ │ │ ├── McpPluginSettings.cs # Plugin configuration +│ │ │ └── IpcClient.cs # IPC with launcher +│ │ ├── Tools/ +│ │ │ ├── Base/ +│ │ │ │ ├── IStrideTool.cs # Tool interface +│ │ │ │ ├── StrideToolBase.cs # Base implementation +│ │ │ │ └── ToolResponse.cs # Common response types +│ │ │ ├── State/ # Read-only operations +│ │ │ │ ├── Assets/ +│ │ │ │ │ ├── ListAssetsTool.cs +│ │ │ │ │ ├── GetAssetMetadataTool.cs +│ │ │ │ │ └── SearchAssetsTool.cs +│ │ │ │ ├── Scenes/ +│ │ │ │ │ ├── ListScenesTool.cs +│ │ │ │ │ ├── GetSceneTreeTool.cs +│ │ │ │ │ └── GetEntityDetailsTool.cs +│ │ │ │ ├── Components/ +│ │ │ │ │ ├── GetComponentsTool.cs +│ │ │ │ │ └── GetComponentDataTool.cs +│ │ │ │ ├── Project/ +│ │ │ │ │ ├── GetProjectInfoTool.cs +│ │ │ │ │ └── GetBuildConfigTool.cs +│ │ │ │ └── UI/ +│ │ │ │ ├── GetUIPageTool.cs +│ │ │ │ └── GetSpriteSheetTool.cs +│ │ │ ├── Navigation/ # Navigation operations +│ │ │ │ ├── OpenSceneTool.cs +│ │ │ │ ├── SelectEntityTool.cs +│ │ │ │ ├── FocusEntityTool.cs +│ │ │ │ ├── OpenAssetFolderTool.cs +│ │ │ │ └── CaptureScreenshotTool.cs +│ │ │ ├── Modification/ # Write operations +│ │ │ │ ├── Entities/ +│ │ │ │ │ ├── CreateEntityTool.cs +│ │ │ │ │ ├── DeleteEntityTool.cs +│ │ │ │ │ ├── MoveEntityTool.cs +│ │ │ │ │ └── ReparentEntityTool.cs +│ │ │ │ ├── Components/ +│ │ │ │ │ ├── AddComponentTool.cs +│ │ │ │ │ ├── RemoveComponentTool.cs +│ │ │ │ │ └── UpdateComponentTool.cs +│ │ │ │ ├── Transforms/ +│ │ │ │ │ ├── SetPositionTool.cs +│ │ │ │ │ ├── SetRotationTool.cs +│ │ │ │ │ └── SetScaleTool.cs +│ │ │ │ ├── Assets/ +│ │ │ │ │ ├── ImportAssetTool.cs +│ │ │ │ │ ├── DeleteAssetTool.cs +│ │ │ │ │ └── RenameAssetTool.cs +│ │ │ │ └── UI/ +│ │ │ │ ├── EditUIPageTool.cs +│ │ │ │ └── EditSpriteSheetTool.cs +│ │ │ └── Lifecycle/ # Lifecycle operations +│ │ │ ├── OpenProjectTool.cs +│ │ │ ├── CreateProjectTool.cs +│ │ │ ├── CloseProjectTool.cs +│ │ │ ├── BuildProjectTool.cs +│ │ │ └── GetBuildStatusTool.cs +│ │ ├── Services/ +│ │ │ ├── SceneAccessService.cs # Thread-safe scene access +│ │ │ ├── AssetAccessService.cs # Thread-safe asset access +│ │ │ ├── EditorStateService.cs # Track editor state +│ │ │ ├── SelectionService.cs # Manage selections +│ │ │ ├── ScreenshotService.cs # Capture screenshots +│ │ │ └── UndoRedoService.cs # Undo/redo integration +│ │ └── Models/ +│ │ ├── EntityData.cs # Serializable entity data +│ │ ├── ComponentData.cs # Serializable component data +│ │ ├── AssetData.cs # Serializable asset data +│ │ └── InstanceInfo.cs # GameStudio instance info +│ │ +│ ├── Stride.GameStudio.McpLauncher/ # NEW: MCP Launcher +│ │ ├── Stride.GameStudio.McpLauncher.csproj +│ │ ├── Program.cs # Main entry point +│ │ ├── McpServer/ +│ │ │ ├── McpServerService.cs # MCP protocol handler +│ │ │ ├── StdioTransport.cs # stdio transport +│ │ │ └── ToolRouter.cs # Route to instances +│ │ ├── InstanceManager/ +│ │ │ ├── GameStudioInstanceManager.cs # Manage instances +│ │ │ ├── GameStudioInstance.cs # Instance wrapper +│ │ │ └── IpcServer.cs # IPC with plugins +│ │ └── Configuration/ +│ │ └── LauncherConfig.cs # Configuration +│ │ +│ └── Stride.GameStudio/ +│ └── Stride.GameStudio.csproj # MODIFIED: Add plugin reference +│ +└── tests/ + ├── Stride.Assets.Presentation.MCP.Tests/ # NEW: Unit tests + │ ├── Stride.Assets.Presentation.MCP.Tests.csproj + │ ├── Tools/ + │ │ ├── State/ + │ │ │ ├── ListAssetsToolTests.cs + │ │ │ ├── GetSceneTreeToolTests.cs + │ │ │ └── GetComponentDataToolTests.cs + │ │ ├── Navigation/ + │ │ │ ├── OpenSceneToolTests.cs + │ │ │ └── SelectEntityToolTests.cs + │ │ └── Modification/ + │ │ ├── CreateEntityToolTests.cs + │ │ └── UpdateComponentToolTests.cs + │ ├── Services/ + │ │ ├── SceneAccessServiceTests.cs + │ │ └── ScreenshotServiceTests.cs + │ └── Mocks/ + │ ├── MockSessionViewModel.cs + │ ├── MockSceneViewModel.cs + │ └── MockAssetViewModel.cs + │ + └── Stride.GameStudio.McpLauncher.IntegrationTests/ # NEW: E2E tests + ├── Stride.GameStudio.McpLauncher.IntegrationTests.csproj + ├── Fixtures/ + │ ├── GameStudioFixture.cs # Launch GameStudio for tests + │ └── TestProjectFixture.cs # Create test projects + ├── EndToEnd/ + │ ├── BasicWorkflowTests.cs # Basic read/write operations + │ ├── MultiInstanceTests.cs # Multi-instance scenarios + │ ├── NavigationTests.cs # Navigation scenarios + │ ├── ModificationTests.cs # Modification scenarios + │ └── LifecycleTests.cs # Launch/close scenarios + └── TestProjects/ # Sample projects for testing + ├── EmptyProject/ + ├── SimpleScene/ + └── ComplexScene/ +``` + +## Implementation Phases + +### Phase 1: Foundation (Week 1-2) + +#### Milestone 1.1: MCP Launcher + Basic IPC +**Deliverables:** +- `Stride.GameStudio.McpLauncher` project created +- stdio transport implementation +- Basic MCP protocol handler (initialize, tools/list) +- IPC infrastructure (named pipes or gRPC) +- Can launch single GameStudio instance +- Basic health check tool + +**Tests:** +- Launcher starts and accepts MCP connections +- Can communicate with launched GameStudio +- Health check returns instance info + +#### Milestone 1.2: MCP Plugin + Core Services +**Deliverables:** +- `Stride.Assets.Presentation.MCP` plugin project +- Plugin loads in GameStudio +- IPC client connects to launcher +- Core services: SceneAccessService, AssetAccessService, EditorStateService +- Thread-safe access patterns established + +**Tests:** +- Plugin loads without errors +- Services can access editor state safely +- No deadlocks under load + +#### Milestone 1.3: First Read Tools +**Deliverables:** +- `ListAssetsTool` - enumerate all assets +- `ListScenesTool` - list all scenes +- `GetSceneTreeTool` - get entity hierarchy +- `GetComponentDataTool` - read component properties + +**Tests:** +- Each tool returns correct data +- Tools handle missing/invalid inputs gracefully +- Performance benchmarks (< 100ms for typical queries) + +### Phase 2: Navigation & Visualization (Week 3-4) + +#### Milestone 2.1: Scene Navigation +**Deliverables:** +- `OpenSceneTool` - open a scene in editor +- `SelectEntityTool` - select entity in scene +- `FocusEntityTool` - focus camera on entity +- `CaptureScreenshotTool` - capture viewport as base64 PNG + +**Tests:** +- Can open scenes and verify they're loaded +- Selection updates correctly in editor +- Screenshots match viewport content + +#### Milestone 2.2: Asset Navigation +**Deliverables:** +- `OpenAssetFolderTool` - navigate to folder in asset view +- `SearchAssetsTool` - search assets by name/type +- `GetAssetMetadataTool` - get detailed asset info + +**Tests:** +- Folder navigation works correctly +- Search returns relevant results +- Metadata is complete and accurate + +#### Milestone 2.3: Extended Visualization +**Deliverables:** +- `GetUIPageTool` - read UI page structure +- `GetSpriteSheetTool` - read sprite sheet data +- Enhanced screenshot capture (UI panels, specific viewports) + +**Tests:** +- UI pages parsed correctly +- Sprite sheets include all frames +- Screenshot capture works for all viewport types + +### Phase 3: Modification Operations (Week 5-6) + +#### Milestone 3.1: Entity Operations +**Deliverables:** +- `CreateEntityTool` - create new entities +- `DeleteEntityTool` - remove entities +- `MoveEntityTool` - change position/rotation/scale +- `ReparentEntityTool` - change parent hierarchy +- Undo/redo integration + +**Tests:** +- Entity operations execute correctly +- Undo/redo works for all operations +- Scene dirty state updates + +#### Milestone 3.2: Component Operations +**Deliverables:** +- `AddComponentTool` - add components to entities +- `RemoveComponentTool` - remove components +- `UpdateComponentTool` - modify component properties +- Type-safe component data serialization + +**Tests:** +- All component types supported +- Property updates persist correctly +- Type validation works + +#### Milestone 3.3: Asset Operations +**Deliverables:** +- `ImportAssetTool` - import files as assets +- `DeleteAssetTool` - remove assets +- `RenameAssetTool` - rename assets +- `EditUIPageTool` - modify UI pages +- `EditSpriteSheetTool` - edit sprite sheets + +**Tests:** +- Asset import handles all formats +- Deletions cascade correctly +- UI/sprite edits persist + +### Phase 4: Lifecycle & Multi-Instance (Week 7-8) + +#### Milestone 4.1: Project Lifecycle +**Deliverables:** +- `OpenProjectTool` - open existing project +- `CreateProjectTool` - create new project from template +- `CloseProjectTool` - close current project +- `BuildProjectTool` - trigger build +- `GetBuildStatusTool` - monitor build progress + +**Tests:** +- Project operations work end-to-end +- Build triggers correctly +- Build status updates in real-time + +#### Milestone 4.2: Multi-Instance Support +**Deliverables:** +- Instance manager tracks multiple GameStudios +- Tool requests include instance ID +- Launcher routes to correct instance +- Instance lifecycle management (launch, monitor, cleanup) + +**Tests:** +- Can manage 3+ simultaneous instances +- Requests route correctly +- Instance crashes handled gracefully + +#### Milestone 4.3: Studio Lifecycle +**Deliverables:** +- Launcher can start GameStudio instances +- Launcher can stop/restart instances +- Automatic crash detection and restart +- Instance affinity (route related operations to same instance) + +**Tests:** +- Launch/close cycles work reliably +- Crash recovery works +- Affinity rules respected + +### Phase 5: Polish & Performance (Week 9-10) + +#### Milestone 5.1: Error Handling & Validation +**Deliverables:** +- Comprehensive input validation +- Detailed error messages +- Error recovery strategies +- Rate limiting and throttling + +**Tests:** +- All error cases covered +- Error messages are actionable +- Rate limiting prevents abuse + +#### Milestone 5.2: Performance Optimization +**Deliverables:** +- Response time < 100ms for reads +- Response time < 500ms for writes +- Batch operation support +- Caching for frequently accessed data + +**Tests:** +- Performance benchmarks pass +- No memory leaks +- Batch operations faster than sequential + +#### Milestone 5.3: Documentation & Examples +**Deliverables:** +- API documentation for all tools +- Usage examples for common workflows +- Claude Desktop integration guide +- Troubleshooting guide + +### Phase 6: Advanced Features (Week 11-12) + +#### Milestone 6.1: Advanced Navigation +**Deliverables:** +- Viewport camera control +- Animation playback control +- Timeline navigation +- Visual debugging overlays + +#### Milestone 6.2: Advanced Modification +**Deliverables:** +- Prefab instantiation +- Script generation +- Material editing +- Animation editing + +#### Milestone 6.3: Monitoring & Events +**Deliverables:** +- MCP Resources for state changes +- Event notifications (entity created, scene loaded, etc.) +- Real-time state synchronization + +## Testing Strategy + +### Unit Tests (Target: 80% coverage) + +**Framework:** xUnit + NSubstitute for mocking + +**Test Categories:** +1. **Tool Tests** - Each tool has comprehensive tests + - Valid inputs return correct results + - Invalid inputs return proper errors + - Edge cases handled + - Performance benchmarks + +2. **Service Tests** - Core services tested in isolation + - Thread safety verified + - Error handling validated + - Resource cleanup verified + +3. **Serialization Tests** - Data models serialize correctly + - All component types supported + - Null handling correct + - Circular references prevented + +**Example Test Structure:** +```csharp +public class GetSceneTreeToolTests +{ + [Fact] + public async Task Execute_ValidSceneId_ReturnsEntityTree() + { + // Arrange + var mockSession = CreateMockSession(); + var tool = new GetSceneTreeTool(mockSession); + var input = new { SceneId = "test-scene" }; + + // Act + var result = await tool.ExecuteAsync(input); + + // Assert + Assert.NotNull(result); + Assert.Equal("RootEntity", result.Name); + Assert.NotEmpty(result.Children); + } + + [Fact] + public async Task Execute_InvalidSceneId_ReturnsError() + { + // ... test error handling + } + + [Fact] + public async Task Execute_PerformanceBenchmark() + { + // ... verify < 100ms response time + } +} +``` + +### Integration Tests (E2E) + +**Framework:** xUnit + real GameStudio instances + +**Test Infrastructure:** +- `GameStudioFixture` - Launches GameStudio for tests +- `TestProjectFixture` - Creates temporary test projects +- Cleanup between tests + +**Test Scenarios:** + +1. **Basic Workflow** (10 tests) + - Launch GameStudio + - List assets + - Open scene + - Read entity tree + - Close project + - Shutdown GameStudio + +2. **Multi-Instance** (5 tests) + - Launch 2 instances + - Open different projects + - Execute tools on each + - Verify correct routing + - Clean shutdown + +3. **Modification Workflow** (15 tests) + - Create entity + - Add components + - Modify properties + - Save scene + - Reload scene + - Verify persistence + +4. **Navigation Workflow** (8 tests) + - Open scene + - Select entity + - Focus camera + - Capture screenshot + - Verify screenshot content + +5. **Lifecycle Workflow** (7 tests) + - Create new project + - Add assets + - Create scene + - Build project + - Verify build output + +**Example E2E Test:** +```csharp +public class BasicWorkflowTests : IClassFixture +{ + private readonly GameStudioFixture _fixture; + + public BasicWorkflowTests(GameStudioFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task CompleteWorkflow_CreateAndModifyEntity() + { + // Arrange - project already opened by fixture + var mcpClient = _fixture.McpClient; + + // Act 1: Create entity + var createResult = await mcpClient.CallToolAsync( + "create_entity", + new { Name = "TestEntity", SceneId = _fixture.MainSceneId } + ); + var entityId = createResult.EntityId; + + // Act 2: Add transform component + await mcpClient.CallToolAsync( + "add_component", + new { EntityId = entityId, ComponentType = "Transform" } + ); + + // Act 3: Update position + await mcpClient.CallToolAsync( + "update_component", + new { + EntityId = entityId, + ComponentType = "Transform", + Properties = new { Position = new { X = 1, Y = 2, Z = 3 } } + } + ); + + // Act 4: Read back entity + var entityData = await mcpClient.CallToolAsync( + "get_entity_details", + new { EntityId = entityId } + ); + + // Assert + Assert.Equal("TestEntity", entityData.Name); + Assert.Single(entityData.Components); + Assert.Equal(1, entityData.Components[0].Properties["Position"].X); + } +} +``` + +### Performance Testing + +**Benchmarks:** +- Read operations: < 100ms +- Write operations: < 500ms +- Screenshot capture: < 2s (1920x1080) +- Multi-instance overhead: < 50ms + +**Load Testing:** +- 100 sequential requests/second sustained +- 10 concurrent requests without deadlock +- Memory usage stable over 1000 operations + +## Configuration + +### Launcher Configuration + +**File:** `stride-mcp-launcher.json` +```json +{ + "mcp": { + "transport": "stdio", + "maxInstances": 5, + "instanceTimeout": 300000, + "enableLogging": true, + "logLevel": "Information" + }, + "gameStudio": { + "executablePath": "C:\\Program Files\\Stride\\GameStudio\\Stride.GameStudio.exe", + "startupArgs": ["--mcp-plugin"], + "workingDirectory": null, + "environmentVariables": { + "STRIDE_MCP_ENABLED": "true" + } + }, + "ipc": { + "transport": "namedPipes", + "pipeName": "stride-mcp-{instanceId}" + }, + "rateLimiting": { + "enabled": true, + "maxRequestsPerSecond": 100, + "maxConcurrentRequests": 10 + } +} +``` + +### Plugin Configuration + +**File:** Embedded in GameStudio settings +```json +{ + "mcpPlugin": { + "enabled": true, + "autoConnect": true, + "launcherPipeName": "stride-mcp-{processId}", + "tools": { + "readOperations": { "enabled": true }, + "writeOperations": { "enabled": true, "requireConfirmation": false }, + "lifecycleOperations": { "enabled": true } + } + } +} +``` + +## Development Workflow + +### Setting Up Development Environment + +1. **Clone Fork:** + ```bash + git clone https://github.com/madsiberian/stride.git + cd stride + ``` + +2. **Create Feature Branch:** + ```bash + git checkout -b feature/mcp-integration + ``` + +3. **Build Prerequisites:** + - .NET 10.0 SDK + - Visual Studio 2026 + - All Stride prerequisites (see main README) + +4. **Build Solution:** + ```bash + cd build + msbuild /t:Restore Stride.sln + msbuild Stride.sln + ``` + +### Development Loop + +1. **Implement Feature** (1-2 days) + - Create/modify source files + - Follow existing Stride code style + +2. **Write Unit Tests** (0.5 days) + - Achieve 80% coverage + - All edge cases covered + +3. **Write Integration Tests** (0.5 days) + - At least one E2E scenario + +4. **Manual Testing** (0.5 days) + - Test with Claude Desktop + - Verify in real GameStudio + +5. **Code Review** (0.5 days) + - Self-review with checklist + - Performance profiling + +6. **Commit & Push** (0.1 days) + - Descriptive commit message + - Reference issue/milestone + +### Testing Before Commit + +```bash +# Run unit tests +dotnet test sources/editor/Stride.Assets.Presentation.MCP.Tests/ + +# Run integration tests (slower) +dotnet test tests/Stride.GameStudio.McpLauncher.IntegrationTests/ + +# Run performance benchmarks +dotnet run --project tests/PerformanceBenchmarks/ --configuration Release +``` + +## Success Criteria + +### Phase 1 Success +- ✅ Launcher starts and manages 1 GameStudio instance +- ✅ 4 read tools implemented and tested +- ✅ Unit tests passing with 80% coverage +- ✅ Basic E2E test passes + +### Phase 2 Success +- ✅ All navigation tools working +- ✅ Screenshots captured successfully +- ✅ Navigation E2E tests passing + +### Phase 3 Success +- ✅ Entity/component CRUD operations working +- ✅ Undo/redo integration complete +- ✅ Modification E2E tests passing + +### Phase 4 Success +- ✅ Multi-instance support (3+ instances) +- ✅ Project lifecycle tools working +- ✅ Build integration complete + +### Phase 5 Success +- ✅ All performance benchmarks met +- ✅ Comprehensive error handling +- ✅ Documentation complete + +### Phase 6 Success +- ✅ Advanced features implemented +- ✅ Event system working +- ✅ Production-ready quality + +## Risk Management + +### Technical Risks + +1. **Threading Issues** + - Risk: Deadlocks when accessing editor state + - Mitigation: All editor access via Dispatcher, strict locking strategy + - Test: Stress tests with concurrent requests + +2. **Memory Leaks** + - Risk: Long-running instances consume excessive memory + - Mitigation: Proper disposal patterns, periodic GC + - Test: Memory profiling over 1000+ operations + +3. **IPC Reliability** + - Risk: IPC connection drops + - Mitigation: Automatic reconnection, heartbeat + - Test: Network fault injection + +4. **Serialization Performance** + - Risk: Large scene trees slow to serialize + - Mitigation: Streaming serialization, pagination + - Test: Benchmark with 10k+ entity scenes + +### Schedule Risks + +1. **Underestimated Complexity** + - Mitigation: Phase 1-2 establish patterns, later phases replicate + - Buffer: 2-week contingency after Phase 3 + +2. **Stride API Changes** + - Mitigation: Work with Stride maintainers, document assumptions + - Buffer: Fork can lag behind upstream + +## Deliverables Checklist + +### Code +- [ ] MCP Launcher project +- [ ] MCP Plugin project +- [ ] Unit test project (80% coverage) +- [ ] Integration test project (30+ E2E tests) +- [ ] All 60+ tools implemented + +### Documentation +- [ ] README for launcher setup +- [ ] API documentation for all tools +- [ ] Architecture decision records +- [ ] Claude Desktop integration guide +- [ ] Troubleshooting guide + +### Quality +- [ ] All tests passing +- [ ] Performance benchmarks met +- [ ] No memory leaks +- [ ] Code review completed +- [ ] Security review completed + +## Next Steps - Starting Implementation + +### Immediate Actions (Day 1) + +1. **Create Launcher Project:** + ```bash + cd sources/editor + dotnet new console -n Stride.GameStudio.McpLauncher + cd Stride.GameStudio.McpLauncher + dotnet add package ModelContextProtocol + dotnet add package System.IO.Pipes # For IPC + ``` + +2. **Create Plugin Project:** + ```bash + cd sources/editor + dotnet new classlib -n Stride.Assets.Presentation.MCP + cd Stride.Assets.Presentation.MCP + dotnet add package ModelContextProtocol + dotnet add package System.IO.Pipes + ``` + +3. **Create Test Projects:** + ```bash + cd tests + dotnet new xunit -n Stride.Assets.Presentation.MCP.Tests + dotnet new xunit -n Stride.GameStudio.McpLauncher.IntegrationTests + ``` + +4. **Add Project References:** + - Plugin → Stride core libraries + - Tests → Plugin + xUnit + NSubstitute + +### Week 1 Goals + +- Complete Milestone 1.1 (Launcher + IPC) +- Complete Milestone 1.2 (Plugin + Services) +- Begin Milestone 1.3 (First tools) + +--- + +**Document Version:** 1.0 +**Last Updated:** 2025-02-17 +**Author:** Development Team +**Status:** APPROVED - Ready for Implementation + +**Estimated Timeline:** 12 weeks (3 months) +**Team Size:** 1-2 developers +**Priority:** High diff --git a/build/Stride.sln b/build/Stride.sln index b28c99bc22..8270ef24a9 100644 --- a/build/Stride.sln +++ b/build/Stride.sln @@ -1,6 +1,7 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11205.157 d18.0 +VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 18.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "90-Tools", "90-Tools", "{1AE1AC60-5D2F-4CA7-AE20-888F44551185}" EndProject @@ -336,14 +337,26 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.FreeImage", "..\sour EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.Editor.CrashReport", "..\sources\editor\Stride.Editor.CrashReport\Stride.Editor.CrashReport.csproj", "{35EC42D8-0A09-41AE-A918-B8C2796061B3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Engine.Mcp", "..\sources\engine\Stride.Engine.Mcp\Stride.Engine.Mcp.csproj", "{F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Engine.Mcp.Tests", "..\sources\engine\Stride.Engine.Mcp.Tests\Stride.Engine.Mcp.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.GameStudio.Mcp", "..\sources\editor\Stride.GameStudio.Mcp\Stride.GameStudio.Mcp.csproj", "{C69117BE-9AF2-4C9E-AE22-E0BD2088F617}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.GameStudio.Mcp.Tests", "..\sources\editor\Stride.GameStudio.Mcp.Tests\Stride.GameStudio.Mcp.Tests.csproj", "{66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|Mixed Platforms = Release|Mixed Platforms Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -351,1178 +364,2106 @@ Global {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|Win32.ActiveCfg = Debug|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|x64.ActiveCfg = Debug|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|x64.Build.0 = Debug|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|x86.ActiveCfg = Debug|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Debug|x86.Build.0 = Debug|Any CPU {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|Any CPU.Build.0 = Release|Any CPU {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|Win32.ActiveCfg = Release|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|x64.ActiveCfg = Release|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|x64.Build.0 = Release|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|x86.ActiveCfg = Release|Any CPU + {2FCA2D8B-B10F-4DCA-9847-4221F74BA586}.Release|x86.Build.0 = Release|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|Any CPU.Build.0 = Debug|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|Win32.ActiveCfg = Debug|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|x64.ActiveCfg = Debug|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|x64.Build.0 = Debug|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|x86.ActiveCfg = Debug|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Debug|x86.Build.0 = Debug|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Release|Any CPU.ActiveCfg = Release|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Release|Any CPU.Build.0 = Release|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C121A566-555E-42B9-9B0A-1696529A9088}.Release|Win32.ActiveCfg = Release|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Release|x64.ActiveCfg = Release|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Release|x64.Build.0 = Release|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Release|x86.ActiveCfg = Release|Any CPU + {C121A566-555E-42B9-9B0A-1696529A9088}.Release|x86.Build.0 = Release|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|Win32.ActiveCfg = Debug|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|x64.ActiveCfg = Debug|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|x64.Build.0 = Debug|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|x86.ActiveCfg = Debug|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Debug|x86.Build.0 = Debug|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|Any CPU.Build.0 = Release|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|Win32.ActiveCfg = Release|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|x64.ActiveCfg = Release|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|x64.Build.0 = Release|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|x86.ActiveCfg = Release|Any CPU + {FB06C76A-6BB7-40BE-9AFA-FEC13B045FB5}.Release|x86.Build.0 = Release|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|Win32.ActiveCfg = Debug|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|x64.ActiveCfg = Debug|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|x64.Build.0 = Debug|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|x86.ActiveCfg = Debug|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Debug|x86.Build.0 = Debug|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|Any CPU.Build.0 = Release|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|Win32.ActiveCfg = Release|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|x64.ActiveCfg = Release|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|x64.Build.0 = Release|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|x86.ActiveCfg = Release|Any CPU + {A8F8D125-7A22-489F-99BC-9A02F545A17F}.Release|x86.Build.0 = Release|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|Any CPU.Build.0 = Debug|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|Win32.ActiveCfg = Debug|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|x64.ActiveCfg = Debug|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|x64.Build.0 = Debug|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|x86.ActiveCfg = Debug|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Debug|x86.Build.0 = Debug|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Release|Any CPU.ActiveCfg = Release|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Release|Any CPU.Build.0 = Release|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {01700344-CF44-482C-BEBC-60213B0F844C}.Release|Win32.ActiveCfg = Release|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Release|x64.ActiveCfg = Release|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Release|x64.Build.0 = Release|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Release|x86.ActiveCfg = Release|Any CPU + {01700344-CF44-482C-BEBC-60213B0F844C}.Release|x86.Build.0 = Release|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|Win32.ActiveCfg = Debug|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|x64.ActiveCfg = Debug|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|x64.Build.0 = Debug|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|x86.ActiveCfg = Debug|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Debug|x86.Build.0 = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|Any CPU.ActiveCfg = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|Any CPU.Build.0 = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|Mixed Platforms.ActiveCfg = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|Mixed Platforms.Build.0 = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|Win32.ActiveCfg = Debug|Any CPU {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|Win32.Build.0 = Debug|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|x64.ActiveCfg = Release|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|x64.Build.0 = Release|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|x86.ActiveCfg = Release|Any CPU + {5AA408BA-E766-453E-B661-E3D7EC46E2A6}.Release|x86.Build.0 = Release|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|Win32.ActiveCfg = Debug|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|x64.Build.0 = Debug|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Debug|x86.Build.0 = Debug|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|Any CPU.Build.0 = Release|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|Win32.ActiveCfg = Release|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|x64.ActiveCfg = Release|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|x64.Build.0 = Release|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|x86.ActiveCfg = Release|Any CPU + {F2D52EDB-BC17-4243-B06D-33CD20F87A7F}.Release|x86.Build.0 = Release|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|Any CPU.Build.0 = Debug|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|Win32.ActiveCfg = Debug|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|x64.ActiveCfg = Debug|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|x64.Build.0 = Debug|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|x86.ActiveCfg = Debug|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Debug|x86.Build.0 = Debug|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|Any CPU.ActiveCfg = Release|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|Any CPU.Build.0 = Release|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|Win32.ActiveCfg = Release|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|x64.ActiveCfg = Release|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|x64.Build.0 = Release|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|x86.ActiveCfg = Release|Any CPU + {47AFCC2E-E9F0-47D6-9D75-9E646546A92B}.Release|x86.Build.0 = Release|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|Any CPU.Build.0 = Debug|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|Win32.ActiveCfg = Debug|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|x64.ActiveCfg = Debug|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|x64.Build.0 = Debug|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|x86.ActiveCfg = Debug|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Debug|x86.Build.0 = Debug|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|Any CPU.Build.0 = Release|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|Win32.ActiveCfg = Release|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|x64.ActiveCfg = Release|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|x64.Build.0 = Release|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|x86.ActiveCfg = Release|Any CPU + {C223FCD7-CDCC-4943-9E11-9C2CC8FA9FC4}.Release|x86.Build.0 = Release|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|Win32.ActiveCfg = Debug|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|x64.Build.0 = Debug|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Debug|x86.Build.0 = Debug|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|Any CPU.Build.0 = Release|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|Win32.ActiveCfg = Release|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|x64.ActiveCfg = Release|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|x64.Build.0 = Release|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|x86.ActiveCfg = Release|Any CPU + {D81F5C91-D7DB-46E5-BC99-49488FB6814C}.Release|x86.Build.0 = Release|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|Any CPU.Build.0 = Debug|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|Win32.ActiveCfg = Debug|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|x64.ActiveCfg = Debug|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|x64.Build.0 = Debug|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|x86.ActiveCfg = Debug|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Debug|x86.Build.0 = Debug|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|Any CPU.ActiveCfg = Release|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|Any CPU.Build.0 = Release|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|Mixed Platforms.Build.0 = Release|Any CPU {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|Win32.ActiveCfg = Release|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|x64.ActiveCfg = Release|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|x64.Build.0 = Release|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|x86.ActiveCfg = Release|Any CPU + {42780CBD-3FE7-48E3-BD5B-59945EA20137}.Release|x86.Build.0 = Release|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|Any CPU.Build.0 = Debug|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|Win32.ActiveCfg = Debug|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|x64.Build.0 = Debug|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Debug|x86.Build.0 = Debug|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|Any CPU.Build.0 = Release|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|Win32.ActiveCfg = Release|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|x64.ActiveCfg = Release|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|x64.Build.0 = Release|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|x86.ActiveCfg = Release|Any CPU + {7F7BFF79-C400-435F-B359-56A2EF8956E0}.Release|x86.Build.0 = Release|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|Any CPU.Build.0 = Debug|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|Win32.ActiveCfg = Debug|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|x64.Build.0 = Debug|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Debug|x86.Build.0 = Debug|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|Any CPU.ActiveCfg = Release|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|Any CPU.Build.0 = Release|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|Win32.ActiveCfg = Release|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|x64.ActiveCfg = Release|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|x64.Build.0 = Release|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|x86.ActiveCfg = Release|Any CPU + {C485CE61-3006-4C99-ACB3-A737F5CEBAE7}.Release|x86.Build.0 = Release|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|Win32.ActiveCfg = Debug|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|x64.ActiveCfg = Debug|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|x64.Build.0 = Debug|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|x86.ActiveCfg = Debug|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Debug|x86.Build.0 = Debug|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|Any CPU.Build.0 = Release|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|Win32.ActiveCfg = Release|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|x64.ActiveCfg = Release|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|x64.Build.0 = Release|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|x86.ActiveCfg = Release|Any CPU + {4B299721-18EA-4B6D-AFD5-2D6E188B97BD}.Release|x86.Build.0 = Release|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|Win32.ActiveCfg = Debug|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|x64.Build.0 = Debug|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|x86.ActiveCfg = Debug|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Debug|x86.Build.0 = Debug|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|Win32.ActiveCfg = Release|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|x64.ActiveCfg = Release|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|x64.Build.0 = Release|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|x86.ActiveCfg = Release|Any CPU + {7AF4B563-AAD3-42FF-B91E-84B9D34D904A}.Release|x86.Build.0 = Release|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|Win32.ActiveCfg = Debug|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|x64.Build.0 = Debug|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Debug|x86.Build.0 = Debug|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|Any CPU.ActiveCfg = Release|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|Mixed Platforms.Build.0 = Release|Any CPU {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|Win32.ActiveCfg = Release|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|x64.ActiveCfg = Release|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|x64.Build.0 = Release|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|x86.ActiveCfg = Release|Any CPU + {09F32307-595A-4CBB-BF7C-F055DA1F70EE}.Release|x86.Build.0 = Release|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|Win32.ActiveCfg = Debug|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|x64.Build.0 = Debug|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|x86.ActiveCfg = Debug|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Debug|x86.Build.0 = Debug|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|Win32.ActiveCfg = Release|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|x64.ActiveCfg = Release|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|x64.Build.0 = Release|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|x86.ActiveCfg = Release|Any CPU + {7732CB84-A39A-4ADF-B740-FD32A352FA8A}.Release|x86.Build.0 = Release|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|Win32.ActiveCfg = Debug|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|x64.ActiveCfg = Debug|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|x64.Build.0 = Debug|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|x86.ActiveCfg = Debug|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Debug|x86.Build.0 = Debug|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|Any CPU.Build.0 = Release|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|Mixed Platforms.Build.0 = Release|Any CPU {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|Win32.ActiveCfg = Release|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|x64.ActiveCfg = Release|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|x64.Build.0 = Release|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|x86.ActiveCfg = Release|Any CPU + {0E916AB7-5A6C-4820-8AB1-AA492FE66D68}.Release|x86.Build.0 = Release|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|Win32.ActiveCfg = Debug|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|x64.ActiveCfg = Debug|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|x64.Build.0 = Debug|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|x86.ActiveCfg = Debug|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Debug|x86.Build.0 = Debug|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|Any CPU.Build.0 = Release|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|Win32.ActiveCfg = Release|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|x64.ActiveCfg = Release|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|x64.Build.0 = Release|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|x86.ActiveCfg = Release|Any CPU + {1677B922-CCF0-44DE-B57E-1CDD3D2B8E8A}.Release|x86.Build.0 = Release|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|Any CPU.Build.0 = Debug|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|Win32.ActiveCfg = Debug|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|x64.ActiveCfg = Debug|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|x64.Build.0 = Debug|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|x86.ActiveCfg = Debug|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Debug|x86.Build.0 = Debug|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|Any CPU.ActiveCfg = Release|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|Any CPU.Build.0 = Release|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|Mixed Platforms.Build.0 = Release|Any CPU {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|Win32.ActiveCfg = Release|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|x64.ActiveCfg = Release|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|x64.Build.0 = Release|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|x86.ActiveCfg = Release|Any CPU + {5210FB81-B807-49BB-AF0D-31FB6A83A572}.Release|x86.Build.0 = Release|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|Win32.ActiveCfg = Debug|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|x64.ActiveCfg = Debug|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|x64.Build.0 = Debug|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|x86.ActiveCfg = Debug|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Debug|x86.Build.0 = Debug|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Release|Any CPU.ActiveCfg = Release|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Release|Any CPU.Build.0 = Release|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1D4210BD-FA51-4709-951B-50647617F97E}.Release|Win32.ActiveCfg = Release|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Release|x64.ActiveCfg = Release|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Release|x64.Build.0 = Release|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Release|x86.ActiveCfg = Release|Any CPU + {1D4210BD-FA51-4709-951B-50647617F97E}.Release|x86.Build.0 = Release|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|Win32.ActiveCfg = Debug|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|x64.ActiveCfg = Debug|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|x64.Build.0 = Debug|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|x86.ActiveCfg = Debug|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Debug|x86.Build.0 = Debug|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|Any CPU.Build.0 = Release|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|Mixed Platforms.Build.0 = Release|Any CPU {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|Win32.ActiveCfg = Release|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|x64.ActiveCfg = Release|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|x64.Build.0 = Release|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|x86.ActiveCfg = Release|Any CPU + {CB6C4D8B-906E-4120-8146-09261B8D2885}.Release|x86.Build.0 = Release|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|Win32.ActiveCfg = Debug|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|x64.Build.0 = Debug|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Debug|x86.Build.0 = Debug|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|Any CPU.Build.0 = Release|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|Win32.ActiveCfg = Release|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|x64.ActiveCfg = Release|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|x64.Build.0 = Release|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|x86.ActiveCfg = Release|Any CPU + {1320F627-EE43-4115-8E89-19D1753E51F2}.Release|x86.Build.0 = Release|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|Win32.ActiveCfg = Debug|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|x64.Build.0 = Debug|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Debug|x86.Build.0 = Debug|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|Any CPU.Build.0 = Release|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|Win32.ActiveCfg = Release|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|x64.ActiveCfg = Release|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|x64.Build.0 = Release|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|x86.ActiveCfg = Release|Any CPU + {1DE01410-22C9-489B-9796-1ADDAB1F64E5}.Release|x86.Build.0 = Release|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|Win32.ActiveCfg = Debug|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|x64.Build.0 = Debug|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|x86.ActiveCfg = Debug|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Debug|x86.Build.0 = Debug|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|Any CPU.Build.0 = Release|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|Win32.ActiveCfg = Release|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|x64.ActiveCfg = Release|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|x64.Build.0 = Release|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|x86.ActiveCfg = Release|Any CPU + {14A47447-2A24-4ECD-B24D-6571499DCD4C}.Release|x86.Build.0 = Release|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|Win32.ActiveCfg = Debug|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|x64.ActiveCfg = Debug|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|x64.Build.0 = Debug|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|x86.ActiveCfg = Debug|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Debug|x86.Build.0 = Debug|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|Any CPU.Build.0 = Release|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|Win32.ActiveCfg = Release|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|x64.ActiveCfg = Release|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|x64.Build.0 = Release|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|x86.ActiveCfg = Release|Any CPU + {273BDD15-7392-4078-91F0-AF23594A3D7B}.Release|x86.Build.0 = Release|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Debug|Win32.ActiveCfg = Debug|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Debug|x64.Build.0 = Debug|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Debug|x86.Build.0 = Debug|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Release|Any CPU.Build.0 = Release|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {DE042125-C270-4D1D-9270-0759C167567A}.Release|Win32.ActiveCfg = Release|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Release|x64.ActiveCfg = Release|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Release|x64.Build.0 = Release|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Release|x86.ActiveCfg = Release|Any CPU + {DE042125-C270-4D1D-9270-0759C167567A}.Release|x86.Build.0 = Release|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|Any CPU.Build.0 = Debug|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|Win32.ActiveCfg = Debug|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|x64.ActiveCfg = Debug|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|x64.Build.0 = Debug|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|x86.ActiveCfg = Debug|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Debug|x86.Build.0 = Debug|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Release|Any CPU.ActiveCfg = Release|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Release|Any CPU.Build.0 = Release|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Release|Mixed Platforms.Build.0 = Release|Any CPU {72390339-B2A1-4F61-A800-31ED0975B515}.Release|Win32.ActiveCfg = Release|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Release|x64.ActiveCfg = Release|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Release|x64.Build.0 = Release|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Release|x86.ActiveCfg = Release|Any CPU + {72390339-B2A1-4F61-A800-31ED0975B515}.Release|x86.Build.0 = Release|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|Win32.ActiveCfg = Debug|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|x64.Build.0 = Debug|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Debug|x86.Build.0 = Debug|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|Any CPU.Build.0 = Release|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|Win32.ActiveCfg = Release|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|x64.ActiveCfg = Release|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|x64.Build.0 = Release|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|x86.ActiveCfg = Release|Any CPU + {E8B3553F-A79F-4E50-B75B-ACEE771C320C}.Release|x86.Build.0 = Release|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|Any CPU.Build.0 = Debug|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|Win32.ActiveCfg = Debug|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|x64.Build.0 = Debug|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Debug|x86.Build.0 = Debug|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|Any CPU.Build.0 = Release|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|Win32.ActiveCfg = Release|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|x64.ActiveCfg = Release|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|x64.Build.0 = Release|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|x86.ActiveCfg = Release|Any CPU + {1BE90177-FE4D-4519-839E-7EB7D78AC973}.Release|x86.Build.0 = Release|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|Win32.ActiveCfg = Debug|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|x64.ActiveCfg = Debug|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|x64.Build.0 = Debug|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|x86.ActiveCfg = Debug|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Debug|x86.Build.0 = Debug|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|Any CPU.Build.0 = Release|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|Win32.ActiveCfg = Release|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|x64.ActiveCfg = Release|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|x64.Build.0 = Release|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|x86.ActiveCfg = Release|Any CPU + {84DEB606-77ED-49CD-9AED-D2B13C1F5A1E}.Release|x86.Build.0 = Release|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|Any CPU.Build.0 = Debug|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|Win32.ActiveCfg = Debug|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|x64.Build.0 = Debug|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Debug|x86.Build.0 = Debug|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|Any CPU.ActiveCfg = Release|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|Any CPU.Build.0 = Release|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|Win32.ActiveCfg = Release|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|x64.ActiveCfg = Release|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|x64.Build.0 = Release|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|x86.ActiveCfg = Release|Any CPU + {1E54A9A2-4439-4444-AE57-6D2ED3C0DC47}.Release|x86.Build.0 = Release|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|Win32.ActiveCfg = Debug|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|x64.ActiveCfg = Debug|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|x64.Build.0 = Debug|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|x86.ActiveCfg = Debug|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Debug|x86.Build.0 = Debug|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|Any CPU.Build.0 = Release|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|Mixed Platforms.Build.0 = Release|Any CPU {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|Win32.ActiveCfg = Release|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|x64.ActiveCfg = Release|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|x64.Build.0 = Release|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|x86.ActiveCfg = Release|Any CPU + {3E7B5D96-CF71-41EE-8CF0-70D090873390}.Release|x86.Build.0 = Release|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|Win32.ActiveCfg = Debug|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|x64.ActiveCfg = Debug|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|x64.Build.0 = Debug|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|x86.ActiveCfg = Debug|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Debug|x86.Build.0 = Debug|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|Any CPU.Build.0 = Release|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|Win32.ActiveCfg = Release|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|x64.ActiveCfg = Release|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|x64.Build.0 = Release|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|x86.ActiveCfg = Release|Any CPU + {39AE9C77-E94B-404F-8768-B6261B3C1E0E}.Release|x86.Build.0 = Release|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|Any CPU.Build.0 = Debug|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|Win32.ActiveCfg = Debug|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|x64.ActiveCfg = Debug|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|x64.Build.0 = Debug|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|x86.ActiveCfg = Debug|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Debug|x86.Build.0 = Debug|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|Any CPU.ActiveCfg = Release|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|Any CPU.Build.0 = Release|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|Win32.ActiveCfg = Release|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|x64.ActiveCfg = Release|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|x64.Build.0 = Release|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|x86.ActiveCfg = Release|Any CPU + {5863574D-7A55-49BC-8E65-BABB74D8E66E}.Release|x86.Build.0 = Release|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|Any CPU.Build.0 = Debug|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|Win32.ActiveCfg = Debug|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|x64.ActiveCfg = Debug|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|x64.Build.0 = Debug|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|x86.ActiveCfg = Debug|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Debug|x86.Build.0 = Debug|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|Any CPU.ActiveCfg = Release|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|Any CPU.Build.0 = Release|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|Mixed Platforms.Build.0 = Release|Any CPU {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|Win32.ActiveCfg = Release|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|x64.ActiveCfg = Release|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|x64.Build.0 = Release|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|x86.ActiveCfg = Release|Any CPU + {50D1A3BB-4B41-4EF5-8D2F-3618A3B6C698}.Release|x86.Build.0 = Release|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|Any CPU.Build.0 = Debug|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|Win32.ActiveCfg = Debug|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|x64.ActiveCfg = Debug|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|x64.Build.0 = Debug|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|x86.ActiveCfg = Debug|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Debug|x86.Build.0 = Debug|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|Any CPU.ActiveCfg = Release|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|Any CPU.Build.0 = Release|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|Mixed Platforms.Build.0 = Release|Any CPU {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|Win32.ActiveCfg = Release|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|x64.ActiveCfg = Release|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|x64.Build.0 = Release|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|x86.ActiveCfg = Release|Any CPU + {117BF9F8-D2D9-4D32-9702-251C3E038090}.Release|x86.Build.0 = Release|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|Win32.ActiveCfg = Debug|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|x64.ActiveCfg = Debug|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|x64.Build.0 = Debug|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|x86.ActiveCfg = Debug|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Debug|x86.Build.0 = Debug|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|Any CPU.Build.0 = Release|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|Win32.ActiveCfg = Release|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|x64.ActiveCfg = Release|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|x64.Build.0 = Release|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|x86.ActiveCfg = Release|Any CPU + {C904D2C6-5A15-4E0B-8432-33967E1735AA}.Release|x86.Build.0 = Release|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|Any CPU.Build.0 = Debug|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|Win32.ActiveCfg = Debug|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|x64.ActiveCfg = Debug|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|x64.Build.0 = Debug|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|x86.ActiveCfg = Debug|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Debug|x86.Build.0 = Debug|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|Any CPU.ActiveCfg = Release|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|Any CPU.Build.0 = Release|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|Mixed Platforms.Build.0 = Release|Any CPU {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|Win32.ActiveCfg = Release|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|x64.ActiveCfg = Release|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|x64.Build.0 = Release|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|x86.ActiveCfg = Release|Any CPU + {49AAA22D-D1C8-4E0F-82E8-F462D5442463}.Release|x86.Build.0 = Release|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|Win32.ActiveCfg = Debug|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|x64.Build.0 = Debug|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Debug|x86.Build.0 = Debug|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|Any CPU.Build.0 = Release|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|Mixed Platforms.Build.0 = Release|Any CPU {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|Win32.ActiveCfg = Release|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|x64.ActiveCfg = Release|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|x64.Build.0 = Release|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|x86.ActiveCfg = Release|Any CPU + {BB9DEEEF-F18C-40D8-B016-6434CC71B8C3}.Release|x86.Build.0 = Release|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|Win32.ActiveCfg = Debug|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|x64.Build.0 = Debug|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Debug|x86.Build.0 = Debug|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|Any CPU.Build.0 = Release|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|Mixed Platforms.Build.0 = Release|Any CPU {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|Win32.ActiveCfg = Release|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|x64.ActiveCfg = Release|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|x64.Build.0 = Release|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|x86.ActiveCfg = Release|Any CPU + {E7B1B17F-D04B-4978-B504-A6BB3EE846C9}.Release|x86.Build.0 = Release|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|Win32.ActiveCfg = Debug|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|x64.Build.0 = Debug|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Debug|x86.Build.0 = Debug|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|Any CPU.Build.0 = Release|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|Win32.ActiveCfg = Release|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|x64.ActiveCfg = Release|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|x64.Build.0 = Release|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|x86.ActiveCfg = Release|Any CPU + {16E02D45-5530-4617-97DC-BC3BDF77DE2C}.Release|x86.Build.0 = Release|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|Any CPU.Build.0 = Debug|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|Win32.ActiveCfg = Debug|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|x64.ActiveCfg = Debug|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|x64.Build.0 = Debug|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|x86.ActiveCfg = Debug|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Debug|x86.Build.0 = Debug|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|Any CPU.ActiveCfg = Release|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|Any CPU.Build.0 = Release|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|Win32.ActiveCfg = Release|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|x64.ActiveCfg = Release|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|x64.Build.0 = Release|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|x86.ActiveCfg = Release|Any CPU + {0EA748AF-E1DC-4788-BA50-8BABD56F220C}.Release|x86.Build.0 = Release|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|Win32.ActiveCfg = Debug|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|x64.Build.0 = Debug|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Debug|x86.Build.0 = Debug|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|Any CPU.Build.0 = Release|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|Mixed Platforms.Build.0 = Release|Any CPU {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|Win32.ActiveCfg = Release|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|x64.ActiveCfg = Release|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|x64.Build.0 = Release|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|x86.ActiveCfg = Release|Any CPU + {66581DAD-70AD-4475-AE47-C6C0DF1EC5E2}.Release|x86.Build.0 = Release|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|Win32.ActiveCfg = Debug|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|x64.Build.0 = Debug|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Debug|x86.Build.0 = Debug|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|Any CPU.Build.0 = Release|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|Mixed Platforms.Build.0 = Release|Any CPU {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|Win32.ActiveCfg = Release|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|x64.ActiveCfg = Release|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|x64.Build.0 = Release|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|x86.ActiveCfg = Release|Any CPU + {D002FEB1-00A6-4AB1-A83F-1F253465E64D}.Release|x86.Build.0 = Release|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|Any CPU.Build.0 = Debug|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|Win32.ActiveCfg = Debug|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|x64.ActiveCfg = Debug|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|x64.Build.0 = Debug|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|x86.ActiveCfg = Debug|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Debug|x86.Build.0 = Debug|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|Any CPU.ActiveCfg = Release|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|Any CPU.Build.0 = Release|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|Mixed Platforms.Build.0 = Release|Any CPU {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|Win32.ActiveCfg = Release|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|x64.ActiveCfg = Release|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|x64.Build.0 = Release|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|x86.ActiveCfg = Release|Any CPU + {942A5B1D-2B3D-4B30-98DE-336CE93F4F12}.Release|x86.Build.0 = Release|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|Win32.ActiveCfg = Debug|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|x64.Build.0 = Debug|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Debug|x86.Build.0 = Debug|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|Any CPU.Build.0 = Release|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|Win32.ActiveCfg = Release|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|x64.ActiveCfg = Release|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|x64.Build.0 = Release|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|x86.ActiveCfg = Release|Any CPU + {2E2382F7-9576-49F0-AE43-93AFD7DB2368}.Release|x86.Build.0 = Release|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|Win32.ActiveCfg = Debug|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|x64.ActiveCfg = Debug|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|x64.Build.0 = Debug|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|x86.ActiveCfg = Debug|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Debug|x86.Build.0 = Debug|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|Any CPU.Build.0 = Release|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|Mixed Platforms.Build.0 = Release|Any CPU {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|Win32.ActiveCfg = Release|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|x64.ActiveCfg = Release|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|x64.Build.0 = Release|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|x86.ActiveCfg = Release|Any CPU + {550C1B7C-B7AD-46DF-ACF3-C36AEF35D5FF}.Release|x86.Build.0 = Release|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|Win32.ActiveCfg = Debug|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|x64.Build.0 = Debug|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Debug|x86.Build.0 = Debug|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|Any CPU.Build.0 = Release|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|Mixed Platforms.Build.0 = Release|Any CPU {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|Win32.ActiveCfg = Release|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|x64.ActiveCfg = Release|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|x64.Build.0 = Release|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|x86.ActiveCfg = Release|Any CPU + {862C7C39-8E2B-4F18-88E9-ACD6EDF818CD}.Release|x86.Build.0 = Release|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|Win32.ActiveCfg = Debug|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|x64.Build.0 = Debug|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Debug|x86.Build.0 = Debug|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|Any CPU.Build.0 = Release|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|Win32.ActiveCfg = Release|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|x64.ActiveCfg = Release|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|x64.Build.0 = Release|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|x86.ActiveCfg = Release|Any CPU + {A5DC820B-9554-45B6-9677-6A2F902E7787}.Release|x86.Build.0 = Release|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|Win32.ActiveCfg = Debug|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|x64.ActiveCfg = Debug|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|x64.Build.0 = Debug|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Debug|x86.Build.0 = Debug|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|Any CPU.Build.0 = Release|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|Win32.ActiveCfg = Release|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|x64.ActiveCfg = Release|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|x64.Build.0 = Release|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|x86.ActiveCfg = Release|Any CPU + {4D13D69B-C8E8-4675-8198-1BE2785FFB6D}.Release|x86.Build.0 = Release|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|Win32.ActiveCfg = Debug|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|x64.ActiveCfg = Debug|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|x64.Build.0 = Debug|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|x86.ActiveCfg = Debug|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Debug|x86.Build.0 = Debug|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Release|Any CPU.Build.0 = Release|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Release|Mixed Platforms.Build.0 = Release|Any CPU {DD592516-B341-40FE-9100-1B0FA784A060}.Release|Win32.ActiveCfg = Release|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Release|x64.ActiveCfg = Release|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Release|x64.Build.0 = Release|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Release|x86.ActiveCfg = Release|Any CPU + {DD592516-B341-40FE-9100-1B0FA784A060}.Release|x86.Build.0 = Release|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|Win32.ActiveCfg = Debug|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|x64.ActiveCfg = Debug|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|x64.Build.0 = Debug|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Debug|x86.Build.0 = Debug|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|Any CPU.Build.0 = Release|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|Win32.ActiveCfg = Release|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|x64.ActiveCfg = Release|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|x64.Build.0 = Release|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|x86.ActiveCfg = Release|Any CPU + {4FAC003A-2532-42F3-AED7-A296D1A1615E}.Release|x86.Build.0 = Release|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|Win32.ActiveCfg = Debug|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|x64.ActiveCfg = Debug|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|x64.Build.0 = Debug|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|x86.ActiveCfg = Debug|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Debug|x86.Build.0 = Debug|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|Any CPU.Build.0 = Release|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|Win32.ActiveCfg = Release|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|x64.ActiveCfg = Release|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|x64.Build.0 = Release|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|x86.ActiveCfg = Release|Any CPU + {FDF801D9-90CC-4CBD-9F53-7F32F7EDF4F1}.Release|x86.Build.0 = Release|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|Win32.ActiveCfg = Debug|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|x64.Build.0 = Debug|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|x86.ActiveCfg = Debug|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Debug|x86.Build.0 = Debug|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|Any CPU.Build.0 = Release|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|Mixed Platforms.Build.0 = Release|Any CPU {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|Win32.ActiveCfg = Release|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|x64.ActiveCfg = Release|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|x64.Build.0 = Release|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|x86.ActiveCfg = Release|Any CPU + {73AA8A18-15C4-405B-BBF4-5D41C1CE44AD}.Release|x86.Build.0 = Release|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|Any CPU.Build.0 = Debug|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|Win32.ActiveCfg = Debug|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|x64.ActiveCfg = Debug|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|x64.Build.0 = Debug|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|x86.ActiveCfg = Debug|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Debug|x86.Build.0 = Debug|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|Any CPU.ActiveCfg = Release|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|Any CPU.Build.0 = Release|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|Mixed Platforms.Build.0 = Release|Any CPU {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|Win32.ActiveCfg = Release|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|x64.ActiveCfg = Release|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|x64.Build.0 = Release|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|x86.ActiveCfg = Release|Any CPU + {77E2FCC0-4CA6-436C-BE6F-9418CB807D45}.Release|x86.Build.0 = Release|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|Any CPU.Build.0 = Debug|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|Win32.ActiveCfg = Debug|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|x64.ActiveCfg = Debug|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|x64.Build.0 = Debug|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|x86.ActiveCfg = Debug|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Debug|x86.Build.0 = Debug|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|Any CPU.ActiveCfg = Release|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|Any CPU.Build.0 = Release|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|Mixed Platforms.Build.0 = Release|Any CPU {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|Win32.ActiveCfg = Release|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|x64.ActiveCfg = Release|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|x64.Build.0 = Release|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|x86.ActiveCfg = Release|Any CPU + {E25E7778-0B2F-4A0B-BCD6-1DE95320B531}.Release|x86.Build.0 = Release|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|Any CPU.Build.0 = Debug|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|Win32.ActiveCfg = Debug|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|Win32.Build.0 = Debug|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|x64.ActiveCfg = Debug|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|x64.Build.0 = Debug|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|x86.ActiveCfg = Debug|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Debug|x86.Build.0 = Debug|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|Any CPU.ActiveCfg = Release|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|Any CPU.Build.0 = Release|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|Mixed Platforms.Build.0 = Release|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|Win32.ActiveCfg = Release|Any CPU {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|Win32.Build.0 = Release|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|x64.ActiveCfg = Release|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|x64.Build.0 = Release|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|x86.ActiveCfg = Release|Any CPU + {63562B0A-E501-42C2-97BB-13D3AD3A7DB4}.Release|x86.Build.0 = Release|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|Win32.ActiveCfg = Debug|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|Win32.Build.0 = Debug|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|x64.ActiveCfg = Debug|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|x64.Build.0 = Debug|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|x86.ActiveCfg = Debug|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Debug|x86.Build.0 = Debug|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|Any CPU.Build.0 = Release|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|Mixed Platforms.Build.0 = Release|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|Win32.ActiveCfg = Release|Any CPU {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|Win32.Build.0 = Release|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|x64.ActiveCfg = Release|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|x64.Build.0 = Release|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|x86.ActiveCfg = Release|Any CPU + {9BC63BEC-F305-451D-BB31-262938EA964D}.Release|x86.Build.0 = Release|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|Any CPU.Build.0 = Debug|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|Win32.ActiveCfg = Debug|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|Win32.Build.0 = Debug|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|x64.Build.0 = Debug|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Debug|x86.Build.0 = Debug|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|Any CPU.ActiveCfg = Release|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|Any CPU.Build.0 = Release|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|Mixed Platforms.Build.0 = Release|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|Win32.ActiveCfg = Release|Any CPU {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|Win32.Build.0 = Release|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|x64.ActiveCfg = Release|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|x64.Build.0 = Release|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|x86.ActiveCfg = Release|Any CPU + {9DE0AA56-0DE7-4ADC-BAAC-CD38B7139EBC}.Release|x86.Build.0 = Release|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|Win32.ActiveCfg = Debug|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|Win32.Build.0 = Debug|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|x64.ActiveCfg = Debug|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|x64.Build.0 = Debug|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Debug|x86.Build.0 = Debug|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|Any CPU.Build.0 = Release|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|Mixed Platforms.Build.0 = Release|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|Win32.ActiveCfg = Release|Any CPU {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|Win32.Build.0 = Release|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|x64.ActiveCfg = Release|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|x64.Build.0 = Release|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|x86.ActiveCfg = Release|Any CPU + {570B0FF9-246F-4C6C-8384-F6BE1887A4A9}.Release|x86.Build.0 = Release|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|Any CPU.Build.0 = Debug|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|Win32.ActiveCfg = Debug|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|Win32.Build.0 = Debug|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|x64.Build.0 = Debug|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|x86.ActiveCfg = Debug|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Debug|x86.Build.0 = Debug|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|Any CPU.Build.0 = Release|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|Win32.ActiveCfg = Release|Any CPU {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|Win32.Build.0 = Release|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|x64.ActiveCfg = Release|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|x64.Build.0 = Release|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|x86.ActiveCfg = Release|Any CPU + {7CA99C7B-E3A2-4DE6-9D6C-314AE39BBBB7}.Release|x86.Build.0 = Release|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|Any CPU.Build.0 = Debug|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|Win32.ActiveCfg = Debug|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|Win32.Build.0 = Debug|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|x64.ActiveCfg = Debug|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|x64.Build.0 = Debug|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|x86.ActiveCfg = Debug|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Debug|x86.Build.0 = Debug|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Release|Any CPU.ActiveCfg = Release|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Release|Any CPU.Build.0 = Release|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Release|Mixed Platforms.Build.0 = Release|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Release|Win32.ActiveCfg = Release|Any CPU {75D71310-ECF7-4592-9E35-3FE540040982}.Release|Win32.Build.0 = Release|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Release|x64.ActiveCfg = Release|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Release|x64.Build.0 = Release|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Release|x86.ActiveCfg = Release|Any CPU + {75D71310-ECF7-4592-9E35-3FE540040982}.Release|x86.Build.0 = Release|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|Any CPU.Build.0 = Debug|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|Win32.ActiveCfg = Debug|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|Win32.Build.0 = Debug|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|x64.ActiveCfg = Debug|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|x64.Build.0 = Debug|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|x86.ActiveCfg = Debug|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Debug|x86.Build.0 = Debug|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|Any CPU.ActiveCfg = Release|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|Any CPU.Build.0 = Release|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|Win32.ActiveCfg = Release|Any CPU {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|Win32.Build.0 = Release|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|x64.ActiveCfg = Release|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|x64.Build.0 = Release|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|x86.ActiveCfg = Release|Any CPU + {F32FDA80-B6DD-47A8-8681-437E2C0D3F31}.Release|x86.Build.0 = Release|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|Win32.ActiveCfg = Debug|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|Win32.Build.0 = Debug|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|x64.Build.0 = Debug|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Debug|x86.Build.0 = Debug|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|Any CPU.Build.0 = Release|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|Mixed Platforms.Build.0 = Release|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|Win32.ActiveCfg = Release|Any CPU {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|Win32.Build.0 = Release|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|x64.ActiveCfg = Release|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|x64.Build.0 = Release|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|x86.ActiveCfg = Release|Any CPU + {B84ECB15-5E3F-4BD1-AB87-333BAE9B70F9}.Release|x86.Build.0 = Release|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|Win32.ActiveCfg = Debug|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|Win32.Build.0 = Debug|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|x64.ActiveCfg = Debug|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|x64.Build.0 = Debug|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|x86.ActiveCfg = Debug|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Debug|x86.Build.0 = Debug|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|Any CPU.ActiveCfg = Release|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|Any CPU.Build.0 = Release|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|Win32.ActiveCfg = Release|Any CPU {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|Win32.Build.0 = Release|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|x64.ActiveCfg = Release|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|x64.Build.0 = Release|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|x86.ActiveCfg = Release|Any CPU + {1DBBC150-F085-43EF-B41D-27C72D133770}.Release|x86.Build.0 = Release|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|Any CPU.Build.0 = Debug|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|Win32.ActiveCfg = Debug|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|Win32.Build.0 = Debug|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|x64.ActiveCfg = Debug|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|x64.Build.0 = Debug|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|x86.ActiveCfg = Debug|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Debug|x86.Build.0 = Debug|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|Any CPU.ActiveCfg = Release|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|Any CPU.Build.0 = Release|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|Mixed Platforms.Build.0 = Release|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|Win32.ActiveCfg = Release|Any CPU {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|Win32.Build.0 = Release|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|x64.ActiveCfg = Release|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|x64.Build.0 = Release|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|x86.ActiveCfg = Release|Any CPU + {370ADF53-DFFA-461E-B72A-1302C0A0DE00}.Release|x86.Build.0 = Release|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|Any CPU.Build.0 = Debug|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|Win32.ActiveCfg = Debug|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|Win32.Build.0 = Debug|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|x64.ActiveCfg = Debug|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|x64.Build.0 = Debug|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|x86.ActiveCfg = Debug|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Debug|x86.Build.0 = Debug|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|Any CPU.ActiveCfg = Release|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|Any CPU.Build.0 = Release|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|Mixed Platforms.Build.0 = Release|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|Win32.ActiveCfg = Release|Any CPU {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|Win32.Build.0 = Release|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|x64.ActiveCfg = Release|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|x64.Build.0 = Release|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|x86.ActiveCfg = Release|Any CPU + {33CC6216-3F30-4B5A-BB29-C5B47EFFA713}.Release|x86.Build.0 = Release|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|Win32.ActiveCfg = Debug|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|Win32.Build.0 = Debug|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|x64.ActiveCfg = Debug|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|x64.Build.0 = Debug|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|x86.ActiveCfg = Debug|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Debug|x86.Build.0 = Debug|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|Any CPU.Build.0 = Release|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|Mixed Platforms.Build.0 = Release|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|Win32.ActiveCfg = Release|Any CPU {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|Win32.Build.0 = Release|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|x64.ActiveCfg = Release|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|x64.Build.0 = Release|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|x86.ActiveCfg = Release|Any CPU + {ACD2C831-BDA2-4512-B4CC-75E8E1804F73}.Release|x86.Build.0 = Release|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|Any CPU.Build.0 = Debug|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|Win32.ActiveCfg = Debug|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|Win32.Build.0 = Debug|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|x64.ActiveCfg = Debug|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|x64.Build.0 = Debug|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|x86.ActiveCfg = Debug|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Debug|x86.Build.0 = Debug|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|Any CPU.ActiveCfg = Release|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|Any CPU.Build.0 = Release|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|Win32.ActiveCfg = Release|Any CPU {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|Win32.Build.0 = Release|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|x64.ActiveCfg = Release|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|x64.Build.0 = Release|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|x86.ActiveCfg = Release|Any CPU + {EFD2472E-B0E1-442A-9057-BBEA2517064B}.Release|x86.Build.0 = Release|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|Win32.ActiveCfg = Debug|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|Win32.Build.0 = Debug|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|x64.Build.0 = Debug|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Debug|x86.Build.0 = Debug|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|Any CPU.Build.0 = Release|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|Mixed Platforms.Build.0 = Release|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|Win32.ActiveCfg = Release|Any CPU {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|Win32.Build.0 = Release|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|x64.ActiveCfg = Release|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|x64.Build.0 = Release|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|x86.ActiveCfg = Release|Any CPU + {25F98E38-0249-45BC-B2ED-7899297B9CF6}.Release|x86.Build.0 = Release|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|Win32.ActiveCfg = Debug|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|Win32.Build.0 = Debug|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|x64.ActiveCfg = Debug|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|x64.Build.0 = Debug|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|x86.ActiveCfg = Debug|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Debug|x86.Build.0 = Debug|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|Any CPU.Build.0 = Release|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|Win32.ActiveCfg = Release|Any CPU {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|Win32.Build.0 = Release|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|x64.ActiveCfg = Release|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|x64.Build.0 = Release|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|x86.ActiveCfg = Release|Any CPU + {BF32DE1B-6276-4341-B212-F8862ADBBA7A}.Release|x86.Build.0 = Release|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|Any CPU.Build.0 = Debug|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|Win32.ActiveCfg = Debug|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|Win32.Build.0 = Debug|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|x64.ActiveCfg = Debug|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|x64.Build.0 = Debug|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|x86.ActiveCfg = Debug|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Debug|x86.Build.0 = Debug|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|Any CPU.ActiveCfg = Release|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|Any CPU.Build.0 = Release|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|Mixed Platforms.Build.0 = Release|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|Win32.ActiveCfg = Release|Any CPU {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|Win32.Build.0 = Release|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|x64.ActiveCfg = Release|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|x64.Build.0 = Release|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|x86.ActiveCfg = Release|Any CPU + {16D8043D-C3DB-4868-BFF3-B2EBDF537AAA}.Release|x86.Build.0 = Release|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|Win32.ActiveCfg = Debug|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|Win32.Build.0 = Debug|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|x64.ActiveCfg = Debug|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|x64.Build.0 = Debug|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|x86.ActiveCfg = Debug|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Debug|x86.Build.0 = Debug|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|Any CPU.Build.0 = Release|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|Mixed Platforms.Build.0 = Release|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|Win32.ActiveCfg = Release|Any CPU {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|Win32.Build.0 = Release|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|x64.ActiveCfg = Release|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|x64.Build.0 = Release|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|x86.ActiveCfg = Release|Any CPU + {0BE7189B-F04E-4C0C-BBE9-F347C0A59FEE}.Release|x86.Build.0 = Release|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|Win32.ActiveCfg = Debug|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|Win32.Build.0 = Debug|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|x64.Build.0 = Debug|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Debug|x86.Build.0 = Debug|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|Any CPU.Build.0 = Release|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|Win32.ActiveCfg = Release|Any CPU {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|Win32.Build.0 = Release|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|x64.ActiveCfg = Release|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|x64.Build.0 = Release|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|x86.ActiveCfg = Release|Any CPU + {4F0E7E04-F067-4CE8-B8C8-1105F319D123}.Release|x86.Build.0 = Release|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|Any CPU.Build.0 = Debug|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|Win32.ActiveCfg = Debug|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|Win32.Build.0 = Debug|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|x64.Build.0 = Debug|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Debug|x86.Build.0 = Debug|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|Any CPU.ActiveCfg = Release|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|Any CPU.Build.0 = Release|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|Win32.ActiveCfg = Release|Any CPU {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|Win32.Build.0 = Release|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|x64.ActiveCfg = Release|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|x64.Build.0 = Release|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|x86.ActiveCfg = Release|Any CPU + {1123EAAD-3FE3-4FD8-8DF6-4DDCF13EFCFB}.Release|x86.Build.0 = Release|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|Win32.ActiveCfg = Debug|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|Win32.Build.0 = Debug|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|x64.Build.0 = Debug|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Debug|x86.Build.0 = Debug|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|Any CPU.Build.0 = Release|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|Win32.ActiveCfg = Release|Any CPU {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|Win32.Build.0 = Release|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|x64.ActiveCfg = Release|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|x64.Build.0 = Release|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|x86.ActiveCfg = Release|Any CPU + {A1A3EB96-46CE-4F2F-A3B6-EF869043DD49}.Release|x86.Build.0 = Release|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|Win32.ActiveCfg = Debug|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|Win32.Build.0 = Debug|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|x64.ActiveCfg = Debug|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|x64.Build.0 = Debug|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|x86.ActiveCfg = Debug|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Debug|x86.Build.0 = Debug|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|Any CPU.Build.0 = Release|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|Mixed Platforms.Build.0 = Release|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|Win32.ActiveCfg = Release|Any CPU {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|Win32.Build.0 = Release|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|x64.ActiveCfg = Release|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|x64.Build.0 = Release|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|x86.ActiveCfg = Release|Any CPU + {53782603-3096-40C2-ABD3-F8F311BAE4BE}.Release|x86.Build.0 = Release|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|Win32.ActiveCfg = Debug|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|Win32.Build.0 = Debug|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|x64.Build.0 = Debug|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Debug|x86.Build.0 = Debug|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|Any CPU.Build.0 = Release|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|Mixed Platforms.Build.0 = Release|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|Win32.ActiveCfg = Release|Any CPU {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|Win32.Build.0 = Release|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|x64.ActiveCfg = Release|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|x64.Build.0 = Release|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|x86.ActiveCfg = Release|Any CPU + {E8C458AE-7B42-4DCE-B326-7F3A9065EA19}.Release|x86.Build.0 = Release|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|Any CPU.Build.0 = Debug|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|Win32.ActiveCfg = Debug|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|Win32.Build.0 = Debug|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|x64.ActiveCfg = Debug|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|x64.Build.0 = Debug|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Debug|x86.Build.0 = Debug|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|Any CPU.ActiveCfg = Release|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|Any CPU.Build.0 = Release|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|Mixed Platforms.Build.0 = Release|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|Win32.ActiveCfg = Release|Any CPU {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|Win32.Build.0 = Release|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|x64.ActiveCfg = Release|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|x64.Build.0 = Release|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|x86.ActiveCfg = Release|Any CPU + {FBE1FA7B-E699-4BB2-9C8F-41F4C9F3F088}.Release|x86.Build.0 = Release|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|Win32.ActiveCfg = Debug|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|Win32.Build.0 = Debug|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|x64.ActiveCfg = Debug|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|x64.Build.0 = Debug|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|x86.ActiveCfg = Debug|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Debug|x86.Build.0 = Debug|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|Any CPU.Build.0 = Release|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|Win32.ActiveCfg = Release|Any CPU {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|Win32.Build.0 = Release|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|x64.ActiveCfg = Release|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|x64.Build.0 = Release|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|x86.ActiveCfg = Release|Any CPU + {1AC5A693-3CC4-4450-AA76-70DA4F0C29DF}.Release|x86.Build.0 = Release|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|Win32.ActiveCfg = Debug|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|Win32.Build.0 = Debug|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|x64.Build.0 = Debug|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Debug|x86.Build.0 = Debug|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|Any CPU.Build.0 = Release|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|Win32.ActiveCfg = Release|Any CPU {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|Win32.Build.0 = Release|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|x64.ActiveCfg = Release|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|x64.Build.0 = Release|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|x86.ActiveCfg = Release|Any CPU + {A9A83BE5-271B-4347-9C4D-340FC3BD0B2B}.Release|x86.Build.0 = Release|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|Win32.ActiveCfg = Debug|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|Win32.Build.0 = Debug|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|x64.ActiveCfg = Debug|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|x64.Build.0 = Debug|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|x86.ActiveCfg = Debug|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Debug|x86.Build.0 = Debug|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|Any CPU.Build.0 = Release|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|Mixed Platforms.Build.0 = Release|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|Win32.ActiveCfg = Release|Any CPU {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|Win32.Build.0 = Release|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|x64.ActiveCfg = Release|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|x64.Build.0 = Release|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|x86.ActiveCfg = Release|Any CPU + {BD176B28-49CD-4FAD-A430-CDBCF1C2E514}.Release|x86.Build.0 = Release|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|Win32.ActiveCfg = Debug|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|Win32.Build.0 = Debug|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|x64.Build.0 = Debug|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Debug|x86.Build.0 = Debug|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|Any CPU.ActiveCfg = Release|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|Any CPU.Build.0 = Release|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|Win32.ActiveCfg = Release|Any CPU {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|Win32.Build.0 = Release|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|x64.ActiveCfg = Release|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|x64.Build.0 = Release|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|x86.ActiveCfg = Release|Any CPU + {7C67FF28-1B9E-4F13-8BDA-B833D588BC6A}.Release|x86.Build.0 = Release|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|Win32.ActiveCfg = Debug|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|Win32.Build.0 = Debug|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|x64.ActiveCfg = Debug|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|x64.Build.0 = Debug|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|x86.ActiveCfg = Debug|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Debug|x86.Build.0 = Debug|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|Any CPU.Build.0 = Release|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|Mixed Platforms.Build.0 = Release|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|Win32.ActiveCfg = Release|Any CPU {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|Win32.Build.0 = Release|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|x64.ActiveCfg = Release|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|x64.Build.0 = Release|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|x86.ActiveCfg = Release|Any CPU + {6A7B231E-36AA-4647-8C1A-FB1540ABC813}.Release|x86.Build.0 = Release|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|Any CPU.Build.0 = Debug|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|Win32.ActiveCfg = Debug|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|Win32.Build.0 = Debug|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|x64.Build.0 = Debug|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Debug|x86.Build.0 = Debug|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|Any CPU.Build.0 = Release|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|Win32.ActiveCfg = Release|Any CPU {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|Win32.Build.0 = Release|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|x64.ActiveCfg = Release|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|x64.Build.0 = Release|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|x86.ActiveCfg = Release|Any CPU + {B686C194-D71D-4FF0-8B4F-F53AFBCD962F}.Release|x86.Build.0 = Release|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|Win32.ActiveCfg = Debug|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|Win32.Build.0 = Debug|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|x64.Build.0 = Debug|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Debug|x86.Build.0 = Debug|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|Any CPU.Build.0 = Release|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|Win32.ActiveCfg = Release|Any CPU {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|Win32.Build.0 = Release|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|x64.ActiveCfg = Release|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|x64.Build.0 = Release|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|x86.ActiveCfg = Release|Any CPU + {164A5B9A-E684-4B3F-9EF4-B7765FC0A8A1}.Release|x86.Build.0 = Release|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|Win32.ActiveCfg = Debug|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|Win32.Build.0 = Debug|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|x64.Build.0 = Debug|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Debug|x86.Build.0 = Debug|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|Any CPU.Build.0 = Release|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|Mixed Platforms.Build.0 = Release|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|Win32.ActiveCfg = Release|Any CPU {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|Win32.Build.0 = Release|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|x64.ActiveCfg = Release|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|x64.Build.0 = Release|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|x86.ActiveCfg = Release|Any CPU + {DA355C86-866F-4843-9B4D-63A173C750FB}.Release|x86.Build.0 = Release|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|Win32.ActiveCfg = Debug|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|Win32.Build.0 = Debug|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|x64.Build.0 = Debug|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Debug|x86.Build.0 = Debug|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|Any CPU.Build.0 = Release|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|Win32.ActiveCfg = Release|Any CPU {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|Win32.Build.0 = Release|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|x64.ActiveCfg = Release|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|x64.Build.0 = Release|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|x86.ActiveCfg = Release|Any CPU + {2FC40214-A4AA-45DC-9C93-72ED800C40B0}.Release|x86.Build.0 = Release|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|Any CPU.Build.0 = Debug|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|Win32.ActiveCfg = Debug|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|Win32.Build.0 = Debug|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|x64.ActiveCfg = Debug|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|x64.Build.0 = Debug|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|x86.ActiveCfg = Debug|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Debug|x86.Build.0 = Debug|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|Any CPU.ActiveCfg = Release|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|Any CPU.Build.0 = Release|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|Win32.ActiveCfg = Release|Any CPU {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|Win32.Build.0 = Release|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|x64.ActiveCfg = Release|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|x64.Build.0 = Release|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|x86.ActiveCfg = Release|Any CPU + {040F754C-17F4-4B5F-B974-93F1E39D107F}.Release|x86.Build.0 = Release|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|Win32.ActiveCfg = Debug|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|Win32.Build.0 = Debug|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|x64.Build.0 = Debug|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Debug|x86.Build.0 = Debug|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|Any CPU.Build.0 = Release|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|Mixed Platforms.Build.0 = Release|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|Win32.ActiveCfg = Release|Any CPU {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|Win32.Build.0 = Release|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|x64.ActiveCfg = Release|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|x64.Build.0 = Release|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|x86.ActiveCfg = Release|Any CPU + {AD4FDC24-B64D-4ED7-91AA-62C9EDA12FA4}.Release|x86.Build.0 = Release|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|Any CPU.Build.0 = Debug|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|Win32.ActiveCfg = Debug|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|Win32.Build.0 = Debug|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|x64.ActiveCfg = Debug|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|x64.Build.0 = Debug|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|x86.ActiveCfg = Debug|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Debug|x86.Build.0 = Debug|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|Any CPU.ActiveCfg = Release|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|Any CPU.Build.0 = Release|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|Mixed Platforms.Build.0 = Release|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|Win32.ActiveCfg = Release|Any CPU {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|Win32.Build.0 = Release|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|x64.ActiveCfg = Release|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|x64.Build.0 = Release|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|x86.ActiveCfg = Release|Any CPU + {66BE41FC-FC52-48D0-9C04-BCE8CC393020}.Release|x86.Build.0 = Release|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|Win32.ActiveCfg = Debug|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|Win32.Build.0 = Debug|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|x64.ActiveCfg = Debug|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|x64.Build.0 = Debug|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|x86.ActiveCfg = Debug|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Debug|x86.Build.0 = Debug|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|Any CPU.Build.0 = Release|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|Mixed Platforms.Build.0 = Release|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|Win32.ActiveCfg = Release|Any CPU {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|Win32.Build.0 = Release|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|x64.ActiveCfg = Release|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|x64.Build.0 = Release|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|x86.ActiveCfg = Release|Any CPU + {D5B023BE-010F-44A8-ABF1-DB6F3BCEA392}.Release|x86.Build.0 = Release|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|Win32.ActiveCfg = Debug|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|Win32.Build.0 = Debug|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|x64.Build.0 = Debug|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Debug|x86.Build.0 = Debug|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Any CPU.Build.0 = Release|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Win32.ActiveCfg = Release|Any CPU {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|Win32.Build.0 = Release|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|x64.ActiveCfg = Release|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|x64.Build.0 = Release|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|x86.ActiveCfg = Release|Any CPU + {1C94168A-3C0D-4C6B-883B-91627D2EF3A1}.Release|x86.Build.0 = Release|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|Win32.ActiveCfg = Debug|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|Win32.Build.0 = Debug|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|x64.ActiveCfg = Debug|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|x64.Build.0 = Debug|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|x86.ActiveCfg = Debug|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Debug|x86.Build.0 = Debug|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|Any CPU.Build.0 = Release|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|Mixed Platforms.Build.0 = Release|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|Win32.ActiveCfg = Release|Any CPU {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|Win32.Build.0 = Release|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|x64.ActiveCfg = Release|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|x64.Build.0 = Release|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|x86.ActiveCfg = Release|Any CPU + {806AA078-6070-4BB6-B05B-6EE6B21B1CDE}.Release|x86.Build.0 = Release|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|Any CPU.Build.0 = Debug|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|Win32.ActiveCfg = Debug|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|Win32.Build.0 = Debug|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|x64.Build.0 = Debug|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Debug|x86.Build.0 = Debug|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|Any CPU.ActiveCfg = Release|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|Any CPU.Build.0 = Release|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|Win32.ActiveCfg = Release|Any CPU {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|Win32.Build.0 = Release|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|x64.ActiveCfg = Release|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|x64.Build.0 = Release|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|x86.ActiveCfg = Release|Any CPU + {D62BBD65-AB1C-41C7-8EC3-88949993C71E}.Release|x86.Build.0 = Release|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|Win32.ActiveCfg = Debug|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|Win32.Build.0 = Debug|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|x64.Build.0 = Debug|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Debug|x86.Build.0 = Debug|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|Any CPU.ActiveCfg = Release|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|Any CPU.Build.0 = Release|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|Mixed Platforms.Build.0 = Release|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|Win32.ActiveCfg = Release|Any CPU {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|Win32.Build.0 = Release|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|x64.ActiveCfg = Release|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|x64.Build.0 = Release|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|x86.ActiveCfg = Release|Any CPU + {BACD76E5-35D0-4389-9BB9-8743AC4D89DE}.Release|x86.Build.0 = Release|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|Any CPU.Build.0 = Debug|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|Win32.ActiveCfg = Debug|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|Win32.Build.0 = Debug|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|x64.ActiveCfg = Debug|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|x64.Build.0 = Debug|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|x86.ActiveCfg = Debug|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Debug|x86.Build.0 = Debug|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|Any CPU.ActiveCfg = Release|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|Any CPU.Build.0 = Release|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|Win32.ActiveCfg = Release|Any CPU {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|Win32.Build.0 = Release|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|x64.ActiveCfg = Release|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|x64.Build.0 = Release|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|x86.ActiveCfg = Release|Any CPU + {09E29A89-A6D7-45C9-B7BA-CA6D643C246F}.Release|x86.Build.0 = Release|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|Win32.ActiveCfg = Debug|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|Win32.Build.0 = Debug|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|x64.Build.0 = Debug|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Debug|x86.Build.0 = Debug|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|Any CPU.Build.0 = Release|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|Win32.ActiveCfg = Release|Any CPU {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|Win32.Build.0 = Release|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|x64.ActiveCfg = Release|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|x64.Build.0 = Release|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|x86.ActiveCfg = Release|Any CPU + {A7FC60AE-BB54-47D3-8787-788EEC65AD45}.Release|x86.Build.0 = Release|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|Any CPU.Build.0 = Debug|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|Win32.ActiveCfg = Debug|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|Win32.Build.0 = Debug|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|x64.ActiveCfg = Debug|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|x64.Build.0 = Debug|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|x86.ActiveCfg = Debug|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Debug|x86.Build.0 = Debug|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|Any CPU.ActiveCfg = Release|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|Any CPU.Build.0 = Release|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|Mixed Platforms.Build.0 = Release|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|Win32.ActiveCfg = Release|Any CPU {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|Win32.Build.0 = Release|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|x64.ActiveCfg = Release|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|x64.Build.0 = Release|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|x86.ActiveCfg = Release|Any CPU + {79F7B3CE-A22F-426D-8DAB-2F692F167210}.Release|x86.Build.0 = Release|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|Any CPU.Build.0 = Debug|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|Win32.ActiveCfg = Debug|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|Win32.Build.0 = Debug|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|x64.ActiveCfg = Debug|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|x64.Build.0 = Debug|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|x86.ActiveCfg = Debug|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Debug|x86.Build.0 = Debug|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|Any CPU.ActiveCfg = Release|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|Any CPU.Build.0 = Release|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|Mixed Platforms.Build.0 = Release|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|Win32.ActiveCfg = Release|Any CPU {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|Win32.Build.0 = Release|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|x64.ActiveCfg = Release|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|x64.Build.0 = Release|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|x86.ActiveCfg = Release|Any CPU + {02FD0BDE-4293-414F-97E6-69FF71105420}.Release|x86.Build.0 = Release|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|Win32.ActiveCfg = Debug|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|Win32.Build.0 = Debug|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|x64.Build.0 = Debug|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|x86.Build.0 = Debug|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Any CPU.Build.0 = Release|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Mixed Platforms.Build.0 = Release|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Win32.ActiveCfg = Release|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Win32.Build.0 = Release|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|x64.ActiveCfg = Release|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|x64.Build.0 = Release|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|x86.ActiveCfg = Release|Any CPU + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|x86.Build.0 = Release|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|Any CPU.Build.0 = Debug|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|Win32.ActiveCfg = Debug|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|Win32.Build.0 = Debug|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|x64.ActiveCfg = Debug|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|x64.Build.0 = Debug|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|x86.ActiveCfg = Debug|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Debug|x86.Build.0 = Debug|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|Any CPU.Build.0 = Release|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|Mixed Platforms.Build.0 = Release|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|Win32.ActiveCfg = Release|Any CPU {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|Win32.Build.0 = Release|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|x64.ActiveCfg = Release|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|x64.Build.0 = Release|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|x86.ActiveCfg = Release|Any CPU + {3E424688-EC44-4DFB-9FC0-4BB1F0683651}.Release|x86.Build.0 = Release|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|Win32.ActiveCfg = Debug|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|Win32.Build.0 = Debug|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|x64.ActiveCfg = Debug|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|x64.Build.0 = Debug|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|x86.ActiveCfg = Debug|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Debug|x86.Build.0 = Debug|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|Any CPU.Build.0 = Release|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|Win32.ActiveCfg = Release|Any CPU {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|Win32.Build.0 = Release|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|x64.ActiveCfg = Release|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|x64.Build.0 = Release|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|x86.ActiveCfg = Release|Any CPU + {7715D094-DF59-4D91-BC9A-9A5118039ECB}.Release|x86.Build.0 = Release|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|Win32.ActiveCfg = Debug|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|Win32.Build.0 = Debug|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|x64.ActiveCfg = Debug|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|x64.Build.0 = Debug|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|x86.ActiveCfg = Debug|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Debug|x86.Build.0 = Debug|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|Any CPU.Build.0 = Release|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|Win32.ActiveCfg = Release|Any CPU {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|Win32.Build.0 = Release|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|x64.ActiveCfg = Release|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|x64.Build.0 = Release|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|x86.ActiveCfg = Release|Any CPU + {66EFFDE4-24F0-4E57-9618-0F5577E20A1E}.Release|x86.Build.0 = Release|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|Win32.ActiveCfg = Debug|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|Win32.Build.0 = Debug|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|x64.ActiveCfg = Debug|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|x64.Build.0 = Debug|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|x86.ActiveCfg = Debug|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Debug|x86.Build.0 = Debug|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|Any CPU.Build.0 = Release|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|Win32.ActiveCfg = Release|Any CPU {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|Win32.Build.0 = Release|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|x64.ActiveCfg = Release|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|x64.Build.0 = Release|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|x86.ActiveCfg = Release|Any CPU + {7B70C783-4085-4702-B3C6-6570FD85CB8F}.Release|x86.Build.0 = Release|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|Win32.ActiveCfg = Debug|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|Win32.Build.0 = Debug|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|x64.ActiveCfg = Debug|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|x64.Build.0 = Debug|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|x86.ActiveCfg = Debug|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Debug|x86.Build.0 = Debug|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|Any CPU.Build.0 = Release|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|Win32.ActiveCfg = Release|Any CPU {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|Win32.Build.0 = Release|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|x64.ActiveCfg = Release|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|x64.Build.0 = Release|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|x86.ActiveCfg = Release|Any CPU + {03695F9B-10E9-4A10-93AE-6402E46F10B5}.Release|x86.Build.0 = Release|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|Win32.ActiveCfg = Debug|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|Win32.Build.0 = Debug|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|x64.Build.0 = Debug|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Debug|x86.Build.0 = Debug|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Any CPU.ActiveCfg = Release|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Any CPU.Build.0 = Release|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Mixed Platforms.Build.0 = Release|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Win32.ActiveCfg = Release|Any CPU {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|Win32.Build.0 = Release|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|x64.ActiveCfg = Release|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|x64.Build.0 = Release|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|x86.ActiveCfg = Release|Any CPU + {35EC42D8-0A09-41AE-A918-B8C2796061B3}.Release|x86.Build.0 = Release|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Debug|Win32.ActiveCfg = Debug|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Debug|Win32.Build.0 = Debug|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Release|Any CPU.Build.0 = Release|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Release|Win32.ActiveCfg = Release|Any CPU + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B}.Release|Win32.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Win32.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Win32.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Win32.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Win32.Build.0 = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|Win32.ActiveCfg = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|Win32.Build.0 = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|x64.ActiveCfg = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|x64.Build.0 = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|x86.ActiveCfg = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Debug|x86.Build.0 = Debug|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|Any CPU.Build.0 = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|Win32.ActiveCfg = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|Win32.Build.0 = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|x64.ActiveCfg = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|x64.Build.0 = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|x86.ActiveCfg = Release|Any CPU + {C69117BE-9AF2-4C9E-AE22-E0BD2088F617}.Release|x86.Build.0 = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|Win32.ActiveCfg = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|Win32.Build.0 = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|x64.Build.0 = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Debug|x86.Build.0 = Debug|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|Any CPU.Build.0 = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|Win32.ActiveCfg = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|Win32.Build.0 = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|x64.ActiveCfg = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|x64.Build.0 = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|x86.ActiveCfg = Release|Any CPU + {66D7CD2E-ACE7-4BBA-A7DF-CA69831502E0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1650,6 +2591,8 @@ Global {7B70C783-4085-4702-B3C6-6570FD85CB8F} = {DE048114-9AE4-467E-A879-188DC0D88A59} {03695F9B-10E9-4A10-93AE-6402E46F10B5} = {1AE1AC60-5D2F-4CA7-AE20-888F44551185} {35EC42D8-0A09-41AE-A918-B8C2796061B3} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} + {F7A1E23C-4F5B-4D8A-9B6E-2C3D4E5F6A7B} = {4C142567-C42B-40F5-B092-798882190209} + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {A7ED9F01-7D78-4381-90A6-D50E51C17250} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FF877973-604D-4EA7-B5F5-A129961F9EF2} diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props index d67bd5c409..dd1edfe083 100644 --- a/sources/Directory.Packages.props +++ b/sources/Directory.Packages.props @@ -43,6 +43,11 @@ + + + + + diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameScreenshotService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameScreenshotService.cs new file mode 100644 index 0000000000..3a2af25bc0 --- /dev/null +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Game/EditorGameScreenshotService.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; +using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; +using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; +using Stride.Editor.EditorGame.Game; +using Stride.Graphics; + +namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game +{ + public class EditorGameScreenshotService : EditorGameServiceBase, IEditorGameScreenshotService + { + private readonly GameEditorViewModel editor; + + private EntityHierarchyEditorGame game; + + public EditorGameScreenshotService(GameEditorViewModel editor) + { + this.editor = editor; + } + + protected override Task Initialize(EditorServiceGame editorGame) + { + if (editorGame == null) throw new ArgumentNullException(nameof(editorGame)); + game = (EntityHierarchyEditorGame)editorGame; + + return Task.FromResult(true); + } + + /// + public async Task CaptureViewportAsync() + { + return await editor.Controller.InvokeAsync(() => + { + var presenter = game.GraphicsDevice.Presenter; + if (presenter?.BackBuffer == null) + throw new InvalidOperationException("Graphics presenter or back buffer is not available."); + + using var stream = new MemoryStream(); + presenter.BackBuffer.Save(game.GraphicsContext.CommandList, stream, ImageFileType.Png); + return stream.ToArray(); + }); + } + } +} diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Services/EntityHierarchyEditorController.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Services/EntityHierarchyEditorController.cs index c537e90e5b..1f18f46a41 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Services/EntityHierarchyEditorController.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Services/EntityHierarchyEditorController.cs @@ -58,6 +58,7 @@ protected override void InitializeServices(EditorGameServiceRegistry services) services.Add(new PhysicsDebugShapeService()); services.Add(new EditorGameLightProbeGizmoService(Editor)); services.Add(new EditorGameCubemapService(Editor)); + services.Add(new EditorGameScreenshotService(Editor)); services.Add(new EditorGameSpaceMarkerService()); services.Add(new EditorGameCameraOrientationService()); services.Add(new EditorGameComponentGizmoService(this)); diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Services/IEditorGameScreenshotService.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Services/IEditorGameScreenshotService.cs new file mode 100644 index 0000000000..dcf15f0874 --- /dev/null +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Services/IEditorGameScreenshotService.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Threading.Tasks; +using Stride.Editor.EditorGame.ViewModels; + +namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services +{ + /// + /// Viewport screenshot capture service. + /// + public interface IEditorGameScreenshotService : IEditorGameViewModelService + { + /// + /// Captures the current viewport as a PNG image. + /// + /// The PNG image data as a byte array. + Task CaptureViewportAsync(); + } +} diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/ViewModels/GameEditorViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/ViewModels/GameEditorViewModel.cs index f348f4a77c..af4e8a3ab9 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/ViewModels/GameEditorViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/ViewModels/GameEditorViewModel.cs @@ -7,6 +7,7 @@ using Stride.Core.Presentation.Commands; using Stride.Core.Presentation.Interop; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; +using Stride.Editor.EditorGame.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels { @@ -53,6 +54,14 @@ protected GameEditorViewModel([NotNull] AssetViewModel asset, [NotNull] Func + /// Gets an editor game service by its interface type. + /// + /// The service interface type. + /// The service instance. + public T GetEditorGameService() where T : IEditorGameViewModelService + => Controller.GetService(); + [NotNull] public ICommandBase CopyErrorToClipboardCommand { get; } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Services/UIEditorController.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Services/UIEditorController.cs index 888ae6c5ec..88e2726df6 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Services/UIEditorController.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/UIEditor/Services/UIEditorController.cs @@ -15,6 +15,7 @@ using Stride.Core.Mathematics; using Stride.Core.Quantum; using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.Services; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game; using Stride.Assets.Presentation.AssetEditors.PrefabEditor.Game; using Stride.Assets.Presentation.AssetEditors.UIEditor.Game; using Stride.Assets.Presentation.AssetEditors.UIEditor.ViewModels; @@ -364,6 +365,7 @@ protected override void InitializeServices(EditorGameServiceRegistry services) services.Add(new UIEditorGameCameraService(this)); services.Add(AdornerService = new UIEditorGameAdornerService(this)); + services.Add(new EditorGameScreenshotService(Editor)); } /// diff --git a/sources/editor/Stride.GameStudio.Mcp.Tests/GameStudioFixture.cs b/sources/editor/Stride.GameStudio.Mcp.Tests/GameStudioFixture.cs new file mode 100644 index 0000000000..465cabc542 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp.Tests/GameStudioFixture.cs @@ -0,0 +1,289 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics; +using System.Text; +using Xunit; + +namespace Stride.GameStudio.Mcp.Tests; + +/// +/// xUnit collection fixture that manages the GameStudio process lifecycle. +/// Launches GameStudio with a sample project, waits for the MCP server to become +/// ready, and kills the process after all tests in the collection complete. +/// +/// This fixture does NOT build GameStudio — run bootstrap.ps1 first. +/// +public sealed class GameStudioFixture : IAsyncLifetime +{ + private const string RelativeExePath = @"sources\editor\Stride.GameStudio\bin\Debug\net10.0-windows\Stride.GameStudio.exe"; + private const string RelativeProjectPath = @"samples\Templates\FirstPersonShooter\FirstPersonShooter.sln"; + private const string RepoRootMarker = @"build\Stride.sln"; + + private static readonly TimeSpan StartupTimeout = TimeSpan.FromSeconds(120); + private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2); + private static readonly TimeSpan ShutdownTimeout = TimeSpan.FromSeconds(10); + + private Process? _process; + private HttpClient? _httpClient; + private string? _tempProjectDir; + private readonly StringBuilder _stdout = new(); + private readonly StringBuilder _stderr = new(); + + /// + /// The port the MCP server is running on. + /// + public int Port { get; private set; } = 5271; + + /// + /// Whether the fixture successfully started GameStudio and the MCP server is ready. + /// + public bool IsReady { get; private set; } + + /// + /// The temporary directory containing the copied test project (solution root). + /// + public string? TempProjectDir => _tempProjectDir; + + private static bool IsEnabled => + string.Equals( + Environment.GetEnvironmentVariable("STRIDE_MCP_INTEGRATION_TESTS"), + "true", + StringComparison.OrdinalIgnoreCase); + + public async Task InitializeAsync() + { + if (!IsEnabled) + return; + + // Read port + var portStr = Environment.GetEnvironmentVariable("STRIDE_MCP_PORT"); + if (int.TryParse(portStr, out var port)) + Port = port; + + // Resolve paths + var exePath = ResolveGameStudioExePath(); + var sourceProjectPath = ResolveTestProjectPath(); + + // Validate + if (!File.Exists(exePath)) + { + throw new InvalidOperationException( + $"Stride.GameStudio.exe not found at: {exePath}\n\n" + + "Run the bootstrap script first to build Game Studio:\n" + + " .\\sources\\editor\\Stride.GameStudio.Mcp.Tests\\bootstrap.ps1\n\n" + + "Or set STRIDE_GAMESTUDIO_EXE to the path of a pre-built executable."); + } + + if (!File.Exists(sourceProjectPath)) + { + throw new InvalidOperationException( + $"Test project not found at: {sourceProjectPath}\n\n" + + "The FirstPersonShooter sample is expected at:\n" + + $" {sourceProjectPath}\n\n" + + "Or set STRIDE_TEST_PROJECT to the path of a .sln to open."); + } + + // Copy the project to a temporary directory so tests don't pollute the source tree. + // GameStudio modifies project files (scene saves, .sln changes, etc.) during normal operation. + var sourceProjectDir = Path.GetDirectoryName(sourceProjectPath)!; + _tempProjectDir = Path.Combine(Path.GetTempPath(), "StrideMcpTests_" + Path.GetRandomFileName()); + CopyDirectory(sourceProjectDir, _tempProjectDir); + var projectPath = Path.Combine(_tempProjectDir, Path.GetFileName(sourceProjectPath)); + + // Launch GameStudio + var startInfo = new ProcessStartInfo + { + FileName = exePath, + Arguments = $"\"{projectPath}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = false, + }; + + // Enable MCP and pass the port to the child process + startInfo.Environment["STRIDE_MCP_ENABLED"] = "true"; + startInfo.Environment["STRIDE_MCP_PORT"] = Port.ToString(); + + _process = Process.Start(startInfo); + if (_process == null) + { + throw new InvalidOperationException("Failed to start GameStudio process."); + } + + _process.OutputDataReceived += (_, args) => + { + if (args.Data != null) + lock (_stdout) { _stdout.AppendLine(args.Data); } + }; + _process.ErrorDataReceived += (_, args) => + { + if (args.Data != null) + lock (_stderr) { _stderr.AppendLine(args.Data); } + }; + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + + // Wait for MCP server to become ready + _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + var endpoint = $"http://localhost:{Port}/sse"; + var deadline = DateTime.UtcNow + StartupTimeout; + + while (DateTime.UtcNow < deadline) + { + if (_process.HasExited) + { + var output = GetCapturedOutput(); + throw new InvalidOperationException( + $"GameStudio exited prematurely with code {_process.ExitCode}.\n\n" + + $"Captured output:\n{output}"); + } + + try + { + // A successful connection to the SSE endpoint means the MCP server is up. + // We don't need to read the SSE stream — just verify it responds. + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var response = await _httpClient.GetAsync(endpoint, HttpCompletionOption.ResponseHeadersRead, cts.Token); + if (response.IsSuccessStatusCode) + { + IsReady = true; + return; + } + } + catch (Exception) when (!_process.HasExited) + { + // Server not ready yet — expected during startup + } + + await Task.Delay(PollInterval); + } + + // Timeout — kill process and report + var timeoutOutput = GetCapturedOutput(); + await KillProcessAsync(); + throw new TimeoutException( + $"GameStudio MCP server did not become ready within {StartupTimeout.TotalSeconds}s.\n" + + $"Endpoint: {endpoint}\n\n" + + $"Captured output:\n{timeoutOutput}"); + } + + public async Task DisposeAsync() + { + await KillProcessAsync(); + _httpClient?.Dispose(); + + // Clean up the temporary project copy + if (_tempProjectDir != null && Directory.Exists(_tempProjectDir)) + { + try + { + Directory.Delete(_tempProjectDir, recursive: true); + } + catch + { + // Best effort — files may still be locked briefly after process exit + } + } + } + + private async Task KillProcessAsync() + { + if (_process == null || _process.HasExited) + { + _process?.Dispose(); + _process = null; + return; + } + + try + { + _process.Kill(entireProcessTree: true); + using var cts = new CancellationTokenSource(ShutdownTimeout); + await _process.WaitForExitAsync(cts.Token); + } + catch (Exception) + { + // Best effort — process may have already exited + } + finally + { + _process.Dispose(); + _process = null; + } + } + + private static string ResolveGameStudioExePath() + { + var envPath = Environment.GetEnvironmentVariable("STRIDE_GAMESTUDIO_EXE"); + if (!string.IsNullOrEmpty(envPath)) + return envPath; + + var repoRoot = FindRepoRoot(); + return Path.Combine(repoRoot, RelativeExePath); + } + + private static string ResolveTestProjectPath() + { + var envPath = Environment.GetEnvironmentVariable("STRIDE_TEST_PROJECT"); + if (!string.IsNullOrEmpty(envPath)) + return envPath; + + var repoRoot = FindRepoRoot(); + return Path.Combine(repoRoot, RelativeProjectPath); + } + + private static string FindRepoRoot() + { + // Walk up from the test assembly directory to find the repo root + var dir = AppContext.BaseDirectory; + while (dir != null) + { + if (File.Exists(Path.Combine(dir, RepoRootMarker))) + return dir; + dir = Path.GetDirectoryName(dir); + } + + throw new InvalidOperationException( + $"Could not locate repository root (looking for '{RepoRootMarker}').\n" + + "Set STRIDE_GAMESTUDIO_EXE and STRIDE_TEST_PROJECT environment variables explicitly."); + } + + private string GetCapturedOutput() + { + var sb = new StringBuilder(); + lock (_stdout) + { + if (_stdout.Length > 0) + { + sb.AppendLine("--- stdout ---"); + sb.Append(_stdout); + } + } + lock (_stderr) + { + if (_stderr.Length > 0) + { + sb.AppendLine("--- stderr ---"); + sb.Append(_stderr); + } + } + return sb.Length > 0 ? sb.ToString() : "(no output captured)"; + } + + private static void CopyDirectory(string sourceDir, string destDir) + { + Directory.CreateDirectory(destDir); + + foreach (var file in Directory.GetFiles(sourceDir)) + { + File.Copy(file, Path.Combine(destDir, Path.GetFileName(file))); + } + + foreach (var dir in Directory.GetDirectories(sourceDir)) + { + CopyDirectory(dir, Path.Combine(destDir, Path.GetFileName(dir))); + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationCollection.cs b/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationCollection.cs new file mode 100644 index 0000000000..bbf2128c1b --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationCollection.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Xunit; + +namespace Stride.GameStudio.Mcp.Tests; + +/// +/// Defines the xUnit test collection for MCP integration tests. +/// All test classes using [Collection("McpIntegration")] share a single +/// instance (one GameStudio process for all tests). +/// +[CollectionDefinition("McpIntegration")] +public class McpIntegrationCollection : ICollectionFixture +{ +} diff --git a/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationFactAttribute.cs b/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationFactAttribute.cs new file mode 100644 index 0000000000..eca2c4503e --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationFactAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Xunit; + +namespace Stride.GameStudio.Mcp.Tests; + +/// +/// A custom Fact attribute that skips the test unless the STRIDE_MCP_INTEGRATION_TESTS +/// environment variable is set to "true". This ensures integration tests that require +/// a running Game Studio instance are disabled by default. +/// +public sealed class McpIntegrationFactAttribute : FactAttribute +{ + private const string EnvVar = "STRIDE_MCP_INTEGRATION_TESTS"; + + public McpIntegrationFactAttribute() + { + if (!string.Equals(Environment.GetEnvironmentVariable(EnvVar), "true", StringComparison.OrdinalIgnoreCase)) + { + Skip = $"MCP integration tests are disabled. Set {EnvVar}=true and ensure Game Studio is running with MCP enabled."; + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationTests.cs b/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationTests.cs new file mode 100644 index 0000000000..9a737fa971 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp.Tests/McpIntegrationTests.cs @@ -0,0 +1,2445 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Text.Json; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using Xunit; + +namespace Stride.GameStudio.Mcp.Tests; + +/// +/// Integration tests for the MCP server embedded in Game Studio. +/// The automatically launches Game Studio and waits +/// for the MCP server to become ready. Tests are skipped unless the environment +/// variable STRIDE_MCP_INTEGRATION_TESTS is set to "true". +/// +/// See README.md in this project for setup instructions. +/// +[Collection("McpIntegration")] +public sealed class McpIntegrationTests : IAsyncLifetime +{ + private readonly GameStudioFixture _fixture; + private McpClient? _client; + + public McpIntegrationTests(GameStudioFixture fixture) + { + _fixture = fixture; + } + + public async Task InitializeAsync() + { + if (!_fixture.IsReady) + return; + + var transport = new HttpClientTransport(new HttpClientTransportOptions + { + Endpoint = new Uri($"http://localhost:{_fixture.Port}/sse"), + Name = "Stride MCP Integration Tests", + }); + + _client = await McpClient.CreateAsync(transport); + } + + public async Task DisposeAsync() + { + if (_client != null) + { + await ((IAsyncDisposable)_client).DisposeAsync(); + } + } + + [McpIntegrationFact] + public async Task GetEditorStatus_ReturnsProjectInfo() + { + var root = await CallToolAndParseJsonAsync("get_editor_status"); + + Assert.Equal("connected", root.GetProperty("status").GetString()); + Assert.False(string.IsNullOrEmpty(root.GetProperty("currentProject").GetString())); + Assert.False(string.IsNullOrEmpty(root.GetProperty("solutionPath").GetString())); + Assert.True(root.GetProperty("assetCount").GetInt32() > 0); + Assert.True(root.GetProperty("scenes").GetArrayLength() > 0); + } + + [McpIntegrationFact] + public async Task McpConfigFile_WritesRuntimeInfo() + { + // The MCP server should write .stride/mcp.json with runtime info on startup + var solutionDir = _fixture.TempProjectDir; + Assert.NotNull(solutionDir); + + var configPath = Path.Combine(solutionDir, ".stride", "mcp.json"); + Assert.True(File.Exists(configPath), $"Expected .stride/mcp.json at: {configPath}"); + + var json = await File.ReadAllTextAsync(configPath); + var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Runtime info should be present with correct values + Assert.True(root.TryGetProperty("runtime", out var runtime), "Expected 'runtime' property in mcp.json"); + Assert.Equal(_fixture.Port, runtime.GetProperty("actualPort").GetInt32()); + Assert.True(runtime.GetProperty("pid").GetInt32() > 0); + Assert.False(string.IsNullOrEmpty(runtime.GetProperty("startedAt").GetString())); + } + + [McpIntegrationFact] + public async Task QueryAssets_ReturnsAssets() + { + var root = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["maxResults"] = 10, + }); + + Assert.True(root.GetProperty("totalCount").GetInt32() > 0); + Assert.True(root.GetProperty("returnedCount").GetInt32() > 0); + + var assets = root.GetProperty("assets"); + Assert.True(assets.GetArrayLength() > 0); + + var first = assets[0]; + Assert.False(string.IsNullOrEmpty(first.GetProperty("id").GetString())); + Assert.False(string.IsNullOrEmpty(first.GetProperty("name").GetString())); + Assert.False(string.IsNullOrEmpty(first.GetProperty("type").GetString())); + } + + [McpIntegrationFact] + public async Task QueryAssets_WithTypeFilter_ReturnsOnlyMatchingType() + { + var root = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["type"] = "SceneAsset", + }); + + var assets = root.GetProperty("assets"); + Assert.True(assets.GetArrayLength() > 0); + foreach (var asset in assets.EnumerateArray()) + { + Assert.Equal("SceneAsset", asset.GetProperty("type").GetString()); + } + } + + [McpIntegrationFact] + public async Task GetSceneTree_ReturnsEntityHierarchy() + { + var sceneId = await GetFirstSceneIdAsync(); + + var root = await CallToolAndParseJsonAsync("get_scene_tree", new Dictionary + { + ["sceneId"] = sceneId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + + var scene = root.GetProperty("scene"); + Assert.Equal(sceneId, scene.GetProperty("id").GetString()); + Assert.False(string.IsNullOrEmpty(scene.GetProperty("name").GetString())); + Assert.True(scene.GetProperty("entityCount").GetInt32() > 0); + + var entities = scene.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0); + + // Verify entity node structure + var firstEntity = entities[0]; + Assert.False(string.IsNullOrEmpty(firstEntity.GetProperty("id").GetString())); + Assert.False(string.IsNullOrEmpty(firstEntity.GetProperty("name").GetString())); + Assert.True(firstEntity.TryGetProperty("components", out _)); + Assert.True(firstEntity.TryGetProperty("children", out _)); + } + + [McpIntegrationFact] + public async Task GetSceneTree_WithInvalidId_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("get_scene_tree", new Dictionary + { + ["sceneId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task GetEntity_ReturnsComponentDetails() + { + var sceneId = await GetFirstSceneIdAsync(); + var entityId = await GetFirstEntityIdAsync(sceneId); + + var root = await CallToolAndParseJsonAsync("get_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + + var entity = root.GetProperty("entity"); + Assert.Equal(entityId, entity.GetProperty("id").GetString()); + Assert.False(string.IsNullOrEmpty(entity.GetProperty("name").GetString())); + Assert.True(entity.TryGetProperty("components", out var components)); + Assert.True(components.GetArrayLength() > 0, "Entity should have at least one component"); + + // Every entity has at least a TransformComponent + var hasTransform = false; + foreach (var comp in components.EnumerateArray()) + { + Assert.False(string.IsNullOrEmpty(comp.GetProperty("type").GetString())); + Assert.True(comp.TryGetProperty("properties", out _)); + if (comp.GetProperty("type").GetString() == "TransformComponent") + hasTransform = true; + } + Assert.True(hasTransform, "Entity should have a TransformComponent"); + } + + [McpIntegrationFact] + public async Task GetEntity_WithInvalidEntityId_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + + var root = await CallToolAndParseJsonAsync("get_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task OpenScene_OpensSceneSuccessfully() + { + var sceneId = await GetFirstSceneIdAsync(); + + var root = await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + Assert.Equal("opened", root.GetProperty("status").GetString()); + Assert.Equal(sceneId, root.GetProperty("sceneId").GetString()); + Assert.False(string.IsNullOrEmpty(root.GetProperty("sceneName").GetString())); + } + + [McpIntegrationFact] + public async Task OpenScene_WithInvalidId_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task SelectEntity_SelectsEntityInEditor() + { + var sceneId = await GetFirstSceneIdAsync(); + // Ensure the scene is open first + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var entityId = await GetFirstEntityIdAsync(sceneId); + + var root = await CallToolAndParseJsonAsync("select_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var selected = root.GetProperty("selected"); + Assert.Equal(1, selected.GetProperty("count").GetInt32()); + var entities = selected.GetProperty("entities"); + Assert.Equal(entityId, entities[0].GetProperty("id").GetString()); + } + + [McpIntegrationFact] + public async Task SelectEntity_WithInvalidEntityId_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + // Ensure the scene is open first + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var root = await CallToolAndParseJsonAsync("select_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task SelectEntity_WithSceneNotOpen_ReturnsError() + { + // Use a scene ID but don't open it — we just call select_entity directly + // Note: this test depends on the editor not having the scene open already from a prior test + // Since open_scene is called in other tests, this may need a fresh scene ID + var root = await CallToolAndParseJsonAsync("select_entity", new Dictionary + { + ["sceneId"] = "00000000-0000-0000-0000-000000000001", + ["entityId"] = "00000000-0000-0000-0000-000000000002", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task FocusEntity_FocusesOnEntity() + { + var sceneId = await GetFirstSceneIdAsync(); + // Ensure the scene is open first + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var entityId = await GetFirstEntityIdAsync(sceneId); + + var root = await CallToolAndParseJsonAsync("focus_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var focused = root.GetProperty("focused"); + Assert.Equal(entityId, focused.GetProperty("id").GetString()); + Assert.False(string.IsNullOrEmpty(focused.GetProperty("name").GetString())); + } + + [McpIntegrationFact] + public async Task FocusEntity_WithInvalidEntityId_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + // Ensure the scene is open first + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var root = await CallToolAndParseJsonAsync("focus_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + // ===================== + // Describe Viewport + // ===================== + + [McpIntegrationFact] + public async Task DescribeViewport_ReturnsVisibleEntities() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("describe_viewport", new Dictionary + { + ["sceneId"] = sceneId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal(sceneId, result.GetProperty("sceneId").GetString()); + + var camera = result.GetProperty("camera"); + Assert.True(camera.TryGetProperty("position", out _)); + Assert.True(camera.TryGetProperty("projection", out _)); + Assert.True(camera.TryGetProperty("fieldOfViewDegrees", out _)); + + var totalEntities = result.GetProperty("totalEntitiesInScene").GetInt32(); + Assert.True(totalEntities > 0, "Expected at least one entity in the scene"); + + var entities = result.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0, "Expected at least one entity in result"); + + var first = entities[0]; + Assert.True(first.TryGetProperty("id", out _)); + Assert.True(first.TryGetProperty("name", out _)); + Assert.True(first.TryGetProperty("worldPosition", out _)); + Assert.True(first.TryGetProperty("distanceToCamera", out _)); + Assert.True(first.TryGetProperty("isVisible", out _)); + Assert.True(first.TryGetProperty("components", out _)); + } + + [McpIntegrationFact] + public async Task DescribeViewport_MaxEntities_LimitsResults() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("describe_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["maxEntities"] = 2, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var entities = root.GetProperty("result").GetProperty("entities"); + Assert.True(entities.GetArrayLength() <= 2); + } + + [McpIntegrationFact] + public async Task DescribeViewport_SceneNotOpen_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("describe_viewport", new Dictionary + { + ["sceneId"] = "00000000-0000-0000-0000-000000000001", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + // ===================== + // Navigate Viewport + // ===================== + + [McpIntegrationFact] + public async Task NavigateViewport_GetCameraState_ReturnsState() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "get_camera_state", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal("get_camera_state", result.GetProperty("action").GetString()); + Assert.True(result.TryGetProperty("position", out var position)); + Assert.True(position.TryGetProperty("x", out _)); + Assert.True(position.TryGetProperty("y", out _)); + Assert.True(position.TryGetProperty("z", out _)); + Assert.True(result.TryGetProperty("yawDegrees", out _)); + Assert.True(result.TryGetProperty("pitchDegrees", out _)); + Assert.True(result.TryGetProperty("projection", out _)); + Assert.True(result.TryGetProperty("fieldOfViewDegrees", out _)); + Assert.True(result.TryGetProperty("nearPlane", out _)); + Assert.True(result.TryGetProperty("farPlane", out _)); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetPosition_MovesCamera() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_position", + ["x"] = 10.0, + ["y"] = 5.0, + ["z"] = 10.0, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal("set_position", result.GetProperty("action").GetString()); + var pos = result.GetProperty("position"); + Assert.Equal(10.0, pos.GetProperty("x").GetDouble(), 0.1); + Assert.Equal(5.0, pos.GetProperty("y").GetDouble(), 0.1); + Assert.Equal(10.0, pos.GetProperty("z").GetDouble(), 0.1); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetPositionWithAngles_SetsYawAndPitch() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_position", + ["x"] = 0.0, + ["y"] = 10.0, + ["z"] = 0.0, + ["yaw"] = 90.0, + ["pitch"] = -45.0, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal(90.0, result.GetProperty("yawDegrees").GetDouble(), 1.0); + Assert.Equal(-45.0, result.GetProperty("pitchDegrees").GetDouble(), 1.0); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetOrientation_Front() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_orientation", + ["orientation"] = "Front", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal("set_orientation", result.GetProperty("action").GetString()); + Assert.Equal("Front", result.GetProperty("orientation").GetString()); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetOrientation_Top() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_orientation", + ["orientation"] = "Top", + }); + + Assert.Null(root.GetProperty("error").GetString()); + Assert.Equal("Top", root.GetProperty("result").GetProperty("orientation").GetString()); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetOrientation_Invalid_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_orientation", + ["orientation"] = "Diagonal", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + Assert.Contains("Diagonal", root.GetProperty("error").GetString()!); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetProjection_Orthographic() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_projection", + ["orthographic"] = true, + }); + + Assert.Null(root.GetProperty("error").GetString()); + Assert.Equal("orthographic", root.GetProperty("result").GetProperty("projection").GetString()); + + // Restore perspective + await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_projection", + ["orthographic"] = false, + }); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetFieldOfView_Perspective() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + // Ensure perspective mode first + await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_projection", + ["orthographic"] = false, + }); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_field_of_view", + ["value"] = 60.0, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal("set_field_of_view", result.GetProperty("action").GetString()); + Assert.Equal("perspective", result.GetProperty("projection").GetString()); + Assert.Equal(60.0, result.GetProperty("fieldOfViewDegrees").GetDouble(), 1.0); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SetPosition_ThenGetState_PositionMatches() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + // Set a specific position + await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "set_position", + ["x"] = 7.0, + ["y"] = 3.0, + ["z"] = 7.0, + }); + + // Wait for camera state to propagate through the game thread + await Task.Delay(1000); + + // Get camera state and verify position + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "get_camera_state", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var pos = root.GetProperty("result").GetProperty("position"); + Assert.Equal(7.0, pos.GetProperty("x").GetDouble(), 0.5); + Assert.Equal(3.0, pos.GetProperty("y").GetDouble(), 0.5); + Assert.Equal(7.0, pos.GetProperty("z").GetDouble(), 0.5); + } + + [McpIntegrationFact] + public async Task NavigateViewport_InvalidAction_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + await Task.Delay(2000); + + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = sceneId, + ["action"] = "fly_around", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + Assert.Contains("fly_around", root.GetProperty("error").GetString()!); + } + + [McpIntegrationFact] + public async Task NavigateViewport_SceneNotOpen_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("navigate_viewport", new Dictionary + { + ["sceneId"] = "00000000-0000-0000-0000-000000000001", + ["action"] = "get_camera_state", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + // ===================== + // Phase 3: Modification + // ===================== + + [McpIntegrationFact] + public async Task CreateEntity_CreatesNewEntity() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var root = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpTestEntity", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var entity = root.GetProperty("entity"); + Assert.False(string.IsNullOrEmpty(entity.GetProperty("id").GetString())); + Assert.Equal("McpTestEntity", entity.GetProperty("name").GetString()); + + // Clean up: delete the created entity + var entityId = entity.GetProperty("id").GetString()!; + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task CreateEntity_WithParent_CreatesChildEntity() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var parentId = await GetFirstEntityIdAsync(sceneId); + + var root = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpChildEntity", + ["parentId"] = parentId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var entity = root.GetProperty("entity"); + Assert.Equal("McpChildEntity", entity.GetProperty("name").GetString()); + Assert.Equal(parentId, entity.GetProperty("parentId").GetString()); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entity.GetProperty("id").GetString()!, + }); + } + + [McpIntegrationFact] + public async Task CreateEntity_WithSceneNotOpen_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = "00000000-0000-0000-0000-000000000001", + ["name"] = "ShouldFail", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task DeleteEntity_DeletesCreatedEntity() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create an entity to delete + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpDeleteMe", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + // Delete it + var root = await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var deleted = root.GetProperty("deleted"); + Assert.Equal(entityId, deleted.GetProperty("id").GetString()); + Assert.Equal("McpDeleteMe", deleted.GetProperty("name").GetString()); + } + + [McpIntegrationFact] + public async Task DeleteEntity_WithInvalidEntityId_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var root = await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task ReparentEntity_MovesToNewParent() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create two entities: one to reparent, one as target parent + var parentResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpNewParent", + }); + var newParentId = parentResult.GetProperty("entity").GetProperty("id").GetString()!; + + var childResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpChildToMove", + }); + var childId = childResult.GetProperty("entity").GetProperty("id").GetString()!; + + // Reparent + var root = await CallToolAndParseJsonAsync("reparent_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = childId, + ["newParentId"] = newParentId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var reparented = root.GetProperty("reparented"); + Assert.Equal(childId, reparented.GetProperty("id").GetString()); + Assert.Equal(newParentId, reparented.GetProperty("newParentId").GetString()); + + // Clean up (delete parent which also deletes child) + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = newParentId, + }); + } + + [McpIntegrationFact] + public async Task ReparentEntity_MoveToRoot() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var parentId = await GetFirstEntityIdAsync(sceneId); + + // Create a child entity under the first entity + var childResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpMoveToRoot", + ["parentId"] = parentId, + }); + var childId = childResult.GetProperty("entity").GetProperty("id").GetString()!; + + // Reparent to root (no newParentId) + var root = await CallToolAndParseJsonAsync("reparent_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = childId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var reparented = root.GetProperty("reparented"); + Assert.Equal(childId, reparented.GetProperty("id").GetString()); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = childId, + }); + } + + [McpIntegrationFact] + public async Task SetTransform_SetsPosition() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpTransformTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + // Set its position + var root = await CallToolAndParseJsonAsync("set_transform", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["positionX"] = 1.0f, + ["positionY"] = 2.0f, + ["positionZ"] = 3.0f, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var transform = root.GetProperty("transform"); + Assert.Equal(entityId, transform.GetProperty("id").GetString()); + + var position = transform.GetProperty("position"); + Assert.Equal(1.0f, position.GetProperty("x").GetSingle(), 0.01f); + Assert.Equal(2.0f, position.GetProperty("y").GetSingle(), 0.01f); + Assert.Equal(3.0f, position.GetProperty("z").GetSingle(), 0.01f); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task SetTransform_SetsRotationAndScale() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpRotScaleTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + // Set rotation and scale + var root = await CallToolAndParseJsonAsync("set_transform", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["rotationX"] = 45.0f, + ["rotationY"] = 90.0f, + ["rotationZ"] = 0.0f, + ["scaleX"] = 2.0f, + ["scaleY"] = 2.0f, + ["scaleZ"] = 2.0f, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var transform = root.GetProperty("transform"); + + var rotation = transform.GetProperty("rotation"); + Assert.Equal(45.0f, rotation.GetProperty("x").GetSingle(), 0.5f); + Assert.Equal(90.0f, rotation.GetProperty("y").GetSingle(), 0.5f); + Assert.Equal(0.0f, rotation.GetProperty("z").GetSingle(), 0.5f); + + var scale = transform.GetProperty("scale"); + Assert.Equal(2.0f, scale.GetProperty("x").GetSingle(), 0.01f); + Assert.Equal(2.0f, scale.GetProperty("y").GetSingle(), 0.01f); + Assert.Equal(2.0f, scale.GetProperty("z").GetSingle(), 0.01f); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task SetTransform_WithInvalidEntityId_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var root = await CallToolAndParseJsonAsync("set_transform", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = "00000000-0000-0000-0000-000000000000", + ["positionX"] = 1.0f, + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + // ============================= + // Phase 3.2: Component Modification + // ============================= + + [McpIntegrationFact] + public async Task ModifyComponent_AddComponent() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpComponentTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + // Add a ModelComponent + var root = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "add", + ["componentType"] = "ModelComponent", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var component = root.GetProperty("component"); + Assert.Equal("added", component.GetProperty("action").GetString()); + Assert.Equal("ModelComponent", component.GetProperty("type").GetString()); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task ModifyComponent_RemoveComponent() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity and add a component to remove + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpRemoveComponentTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "add", + ["componentType"] = "ModelComponent", + }); + + // Remove the added component (index 1, since 0 is TransformComponent) + var root = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "remove", + ["componentIndex"] = 1, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var component = root.GetProperty("component"); + Assert.Equal("removed", component.GetProperty("action").GetString()); + Assert.Equal("ModelComponent", component.GetProperty("type").GetString()); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task ModifyComponent_RemoveTransform_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var entityId = await GetFirstEntityIdAsync(sceneId); + + var root = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "remove", + ["componentIndex"] = 0, + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task ModifyComponent_InvalidAction_ReturnsError() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var entityId = await GetFirstEntityIdAsync(sceneId); + + var root = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "invalid", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + // ===================== + // Viewport + // ===================== + + [McpIntegrationFact] + public async Task CaptureViewport_WithSceneNotOpen_ReturnsError() + { + var result = await _client!.CallToolAsync("capture_viewport", new Dictionary + { + ["assetId"] = "00000000-0000-0000-0000-000000000001", + }); + + var textBlock = result.Content.OfType().FirstOrDefault(); + Assert.NotNull(textBlock); + Assert.Contains("not found", textBlock.Text!, StringComparison.OrdinalIgnoreCase); + } + + // ===================== + // Phase 5: Asset Management + // ===================== + + [McpIntegrationFact] + public async Task GetAssetDetails_ReturnsAssetProperties() + { + // Get a known asset ID from query_assets + var queryRoot = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["maxResults"] = 1, + }); + var firstAsset = queryRoot.GetProperty("assets")[0]; + var assetId = firstAsset.GetProperty("id").GetString()!; + + var root = await CallToolAndParseJsonAsync("get_asset_details", new Dictionary + { + ["assetId"] = assetId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + + var asset = root.GetProperty("asset"); + Assert.Equal(assetId, asset.GetProperty("id").GetString()); + Assert.False(string.IsNullOrEmpty(asset.GetProperty("name").GetString())); + Assert.False(string.IsNullOrEmpty(asset.GetProperty("type").GetString())); + Assert.True(asset.TryGetProperty("properties", out _)); + } + + [McpIntegrationFact] + public async Task GetAssetDetails_WithInvalidId_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("get_asset_details", new Dictionary + { + ["assetId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task GetAssetDependencies_ReturnsDependencyInfo() + { + // Get a known asset ID + var queryRoot = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["maxResults"] = 1, + }); + var assetId = queryRoot.GetProperty("assets")[0].GetProperty("id").GetString()!; + + var root = await CallToolAndParseJsonAsync("get_asset_dependencies", new Dictionary + { + ["assetId"] = assetId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + + var deps = root.GetProperty("dependencies"); + Assert.Equal(assetId, deps.GetProperty("assetId").GetString()); + Assert.True(deps.TryGetProperty("referencedBy", out _)); + Assert.True(deps.TryGetProperty("references", out _)); + Assert.True(deps.TryGetProperty("brokenReferences", out _)); + } + + [McpIntegrationFact] + public async Task GetAssetDependencies_WithInvalidId_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("get_asset_dependencies", new Dictionary + { + ["assetId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task CreateAsset_CreatesAndDeletesMaterial() + { + var root = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "MaterialAsset", + ["name"] = "McpTestMaterial", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var asset = root.GetProperty("asset"); + Assert.False(string.IsNullOrEmpty(asset.GetProperty("id").GetString())); + Assert.Equal("McpTestMaterial", asset.GetProperty("name").GetString()); + Assert.Equal("MaterialAsset", asset.GetProperty("type").GetString()); + + // Clean up: delete the asset + var assetId = asset.GetProperty("id").GetString()!; + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = assetId, + ["action"] = "delete", + }); + } + + [McpIntegrationFact] + public async Task CreateAsset_WithInvalidType_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "NonExistentAssetType", + ["name"] = "ShouldFail", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task ManageAsset_RenameAsset() + { + // Create an asset to rename + var createRoot = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "MaterialAsset", + ["name"] = "McpRenameMe", + }); + var assetId = createRoot.GetProperty("asset").GetProperty("id").GetString()!; + + // Rename it + var root = await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = assetId, + ["action"] = "rename", + ["newName"] = "McpRenamed", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal("renamed", result.GetProperty("action").GetString()); + Assert.Equal("McpRenameMe", result.GetProperty("oldName").GetString()); + Assert.Equal("McpRenamed", result.GetProperty("newName").GetString()); + + // Clean up + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = assetId, + ["action"] = "delete", + }); + } + + [McpIntegrationFact] + public async Task ManageAsset_InvalidAction_ReturnsError() + { + var queryRoot = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["maxResults"] = 1, + }); + var assetId = queryRoot.GetProperty("assets")[0].GetProperty("id").GetString()!; + + var root = await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = assetId, + ["action"] = "invalid_action", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task SetAssetProperty_WithInvalidPath_ReturnsError() + { + // Use a MaterialAsset — it always has a property graph, unlike some source-file assets + var queryRoot = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["type"] = "MaterialAsset", + ["maxResults"] = 1, + }); + var assetId = queryRoot.GetProperty("assets")[0].GetProperty("id").GetString()!; + + var root = await CallToolAndParseJsonAsync("set_asset_property", new Dictionary + { + ["assetId"] = assetId, + ["propertyPath"] = "NonExistentProperty", + ["value"] = "42", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + // The error should include available property names + Assert.Contains("Available properties", root.GetProperty("error").GetString()!); + } + + [McpIntegrationFact] + public async Task SaveProject_ReturnsSaveResult() + { + var root = await CallToolAndParseJsonAsync("save_project"); + + // save_project should either succeed or return a meaningful error + var result = root.GetProperty("result"); + Assert.True(result.TryGetProperty("status", out var status)); + Assert.False(string.IsNullOrEmpty(status.GetString())); + } + + // ===================== + // Phase 4: Build Tools + // ===================== + + [McpIntegrationFact] + public async Task GetBuildStatus_WhenIdle_ReturnsIdle() + { + var root = await CallToolAndParseJsonAsync("get_build_status"); + // Status should be idle or reflect a prior build + Assert.True(root.TryGetProperty("status", out var status)); + Assert.False(string.IsNullOrEmpty(status.GetString())); + } + + [McpIntegrationFact] + public async Task ListTools_ReturnsAllExpectedTools() + { + var tools = await _client!.ListToolsAsync(); + var toolNames = tools.Select(t => t.Name).ToHashSet(); + + // Phase 1 tools + Assert.Contains("get_editor_status", toolNames); + Assert.Contains("query_assets", toolNames); + Assert.Contains("get_scene_tree", toolNames); + Assert.Contains("get_entity", toolNames); + + // Phase 2 tools (Navigation & Selection) + Assert.Contains("open_scene", toolNames); + Assert.Contains("select_entity", toolNames); + Assert.Contains("focus_entity", toolNames); + + // Phase 3 tools (Modification) + Assert.Contains("create_entity", toolNames); + Assert.Contains("delete_entity", toolNames); + Assert.Contains("reparent_entity", toolNames); + Assert.Contains("set_transform", toolNames); + Assert.Contains("modify_component", toolNames); + + // Viewport tools + Assert.Contains("capture_viewport", toolNames); + Assert.Contains("describe_viewport", toolNames); + Assert.Contains("navigate_viewport", toolNames); + + // Phase 4 tools (Build) + Assert.Contains("build_project", toolNames); + Assert.Contains("get_build_status", toolNames); + + // Phase 5 tools (Asset Management) + Assert.Contains("get_asset_details", toolNames); + Assert.Contains("get_asset_dependencies", toolNames); + Assert.Contains("create_asset", toolNames); + Assert.Contains("manage_asset", toolNames); + Assert.Contains("set_asset_property", toolNames); + Assert.Contains("save_project", toolNames); + + // Reload tools + Assert.Contains("reload_scene", toolNames); + Assert.Contains("restart_game_studio", toolNames); + Assert.Contains("reload_assemblies", toolNames); + + // UI Page tools + Assert.Contains("get_ui_tree", toolNames); + Assert.Contains("get_ui_element", toolNames); + Assert.Contains("add_ui_element", toolNames); + Assert.Contains("remove_ui_element", toolNames); + Assert.Contains("set_ui_element_property", toolNames); + + // Sprite tools + Assert.Contains("add_sprite_frame", toolNames); + Assert.Contains("remove_sprite_frame", toolNames); + + // Project tools + Assert.Contains("set_active_project", toolNames); + Assert.Contains("manage_root_assets", toolNames); + + // UI navigation + Assert.Contains("open_ui_page", toolNames); + } + + // ===================== + // Reload Tools + // ===================== + + [McpIntegrationFact] + public async Task ReloadScene_ReloadsOpenScene() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + var root = await CallToolAndParseJsonAsync("reload_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + Assert.Equal("reloaded", result.GetProperty("status").GetString()); + Assert.Equal(sceneId, result.GetProperty("sceneId").GetString()); + } + + [McpIntegrationFact] + public async Task ReloadScene_WithInvalidId_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("reload_scene", new Dictionary + { + ["sceneId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task ReloadAssemblies_Status_ReturnsState() + { + var root = await CallToolAndParseJsonAsync("reload_assemblies", new Dictionary + { + ["action"] = "status", + }); + + Assert.Null(root.GetProperty("error").GetString()); + var result = root.GetProperty("result"); + // Should have assemblyReloadPending field (true or false) + Assert.True(result.TryGetProperty("assemblyReloadPending", out var pending)); + Assert.True(pending.ValueKind == JsonValueKind.True || pending.ValueKind == JsonValueKind.False); + Assert.False(string.IsNullOrEmpty(result.GetProperty("message").GetString())); + } + + [McpIntegrationFact] + public async Task ReloadAssemblies_InvalidAction_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("reload_assemblies", new Dictionary + { + ["action"] = "invalid_action", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task GetBuildStatus_IncludesAssemblyReloadPending() + { + var root = await CallToolAndParseJsonAsync("get_build_status"); + + Assert.True(root.TryGetProperty("assemblyReloadPending", out var pending)); + Assert.True(pending.ValueKind == JsonValueKind.True || pending.ValueKind == JsonValueKind.False); + } + + // ===================== + // Asset Reference Update + // ===================== + + [McpIntegrationFact] + public async Task ModifyComponent_UpdateWithAssetReference() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity with a ModelComponent + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpAssetRefTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "add", + ["componentType"] = "ModelComponent", + }); + + // Find a model asset to reference + var queryRoot = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["type"] = "ModelAsset", + ["maxResults"] = 1, + }); + + var assets = queryRoot.GetProperty("assets"); + if (assets.GetArrayLength() > 0) + { + var modelAssetId = assets[0].GetProperty("id").GetString()!; + + // Update ModelComponent.Model with asset reference + var updateRoot = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "update", + ["componentIndex"] = 1, + ["properties"] = JsonSerializer.Serialize(new { Model = new { assetId = modelAssetId } }), + }); + + Assert.Null(updateRoot.GetProperty("error").GetString()); + var component = updateRoot.GetProperty("component"); + Assert.Contains("Model", component.GetProperty("updatedProperties").EnumerateArray().Select(e => e.GetString()!)); + } + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task GetEntity_ReturnsAssetReferenceProperties() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity with a ModelComponent + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpAssetRefReadTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "add", + ["componentType"] = "ModelComponent", + }); + + // Find a model asset to reference + var queryRoot = await CallToolAndParseJsonAsync("query_assets", new Dictionary + { + ["type"] = "ModelAsset", + ["maxResults"] = 1, + }); + + var assets = queryRoot.GetProperty("assets"); + if (assets.GetArrayLength() > 0) + { + var modelAssetId = assets[0].GetProperty("id").GetString()!; + + // Set the Model property + await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "update", + ["componentIndex"] = 1, + ["properties"] = JsonSerializer.Serialize(new { Model = new { assetId = modelAssetId } }), + }); + + // Now read the entity back and verify the asset reference is visible + var entityRoot = await CallToolAndParseJsonAsync("get_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + + Assert.Null(entityRoot.GetProperty("error").GetString()); + var entity = entityRoot.GetProperty("entity"); + var components = entity.GetProperty("components"); + + // Find the ModelComponent (index 1, after TransformComponent) + var modelComp = components.EnumerateArray() + .FirstOrDefault(c => c.GetProperty("type").GetString() == "ModelComponent"); + Assert.True(modelComp.ValueKind != JsonValueKind.Undefined, "ModelComponent should exist"); + + var props = modelComp.GetProperty("properties"); + Assert.True(props.TryGetProperty("Model", out var modelProp), "Model property should be serialized"); + + // The Model property should contain the asset reference with assetRef and url + Assert.True(modelProp.TryGetProperty("assetRef", out var assetRefValue), + $"Model property should have 'assetRef' field. Actual value: {modelProp}"); + Assert.Equal(modelAssetId, assetRefValue.GetString()); + } + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + // ===================== + // Polymorphic Properties + // ===================== + + [McpIntegrationFact] + public async Task ModifyComponent_SetPolymorphicLightType() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity with a LightComponent + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpPolymorphicLightTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "add", + ["componentType"] = "LightComponent", + }); + + // Change the light type to LightPoint using polymorphic property + var updateRoot = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "update", + ["componentIndex"] = 1, + ["properties"] = JsonSerializer.Serialize(new { Type = "LightPoint" }), + }); + + Assert.Null(updateRoot.GetProperty("error").GetString()); + var component = updateRoot.GetProperty("component"); + Assert.Contains("Type", component.GetProperty("updatedProperties").EnumerateArray().Select(e => e.GetString()!)); + + // Verify via get_entity that the light type changed + var entityRoot = await CallToolAndParseJsonAsync("get_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + var entity = entityRoot.GetProperty("entity"); + var components = entity.GetProperty("components"); + var lightComp = components.EnumerateArray() + .FirstOrDefault(c => c.GetProperty("type").GetString() == "LightComponent"); + Assert.NotEqual(default, lightComp); + var typeProperty = lightComp.GetProperty("properties").GetProperty("Type"); + Assert.Contains("LightPoint", typeProperty.ToString()); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task SetAssetProperty_SetPolymorphicMaterialFeature() + { + // Create a MaterialAsset + var createRoot = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "MaterialAsset", + ["name"] = "McpPolyMaterialTest", + }); + Assert.Null(createRoot.GetProperty("error").GetString()); + var assetId = createRoot.GetProperty("asset").GetProperty("id").GetString()!; + + // Set Attributes.Diffuse to MaterialDiffuseMapFeature + var setRoot = await CallToolAndParseJsonAsync("set_asset_property", new Dictionary + { + ["assetId"] = assetId, + ["propertyPath"] = "Attributes.Diffuse", + ["value"] = "\"MaterialDiffuseMapFeature\"", + }); + + Assert.Null(setRoot.GetProperty("error").GetString()); + + // Verify via get_asset_details + var detailsRoot = await CallToolAndParseJsonAsync("get_asset_details", new Dictionary + { + ["assetId"] = assetId, + }); + Assert.Null(detailsRoot.GetProperty("error").GetString()); + var properties = detailsRoot.GetProperty("asset").GetProperty("properties"); + Assert.Contains("MaterialDiffuseMapFeature", properties.ToString()); + + // Clean up + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = assetId, + ["action"] = "delete", + }); + } + + [McpIntegrationFact] + public async Task SetAssetProperty_PolymorphicWithInlineProperties() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity with a LightComponent + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpPolyInlineTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "add", + ["componentType"] = "LightComponent", + }); + + // Set the light type to LightPoint with inline properties using $type format + var updateRoot = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "update", + ["componentIndex"] = 1, + ["properties"] = "{\"Type\":{\"$type\":\"LightPoint\",\"Radius\":5.0}}", + }); + + Assert.Null(updateRoot.GetProperty("error").GetString()); + + // Verify via get_entity + var entityRoot = await CallToolAndParseJsonAsync("get_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + var entity = entityRoot.GetProperty("entity"); + var components = entity.GetProperty("components"); + var lightComp = components.EnumerateArray() + .FirstOrDefault(c => c.GetProperty("type").GetString() == "LightComponent"); + Assert.NotEqual(default, lightComp); + Assert.Contains("LightPoint", lightComp.GetProperty("properties").GetProperty("Type").ToString()); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + [McpIntegrationFact] + public async Task SetAssetProperty_InvalidPolymorphicType_ListsAvailable() + { + var sceneId = await GetFirstSceneIdAsync(); + await CallToolAndParseJsonAsync("open_scene", new Dictionary + { + ["sceneId"] = sceneId, + }); + + // Create a test entity with a LightComponent + var createResult = await CallToolAndParseJsonAsync("create_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["name"] = "McpPolyInvalidTest", + }); + var entityId = createResult.GetProperty("entity").GetProperty("id").GetString()!; + + await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "add", + ["componentType"] = "LightComponent", + }); + + // Try to set an invalid polymorphic type name + var updateRoot = await CallToolAndParseJsonAsync("modify_component", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + ["action"] = "update", + ["componentIndex"] = 1, + ["properties"] = JsonSerializer.Serialize(new { Type = "LightCone" }), + }); + + // The error should mention the invalid type and list available types + Assert.False(string.IsNullOrEmpty(updateRoot.GetProperty("error").GetString())); + var error = updateRoot.GetProperty("error").GetString()!; + Assert.Contains("LightCone", error); + Assert.Contains("Available types", error); + + // Clean up + await CallToolAndParseJsonAsync("delete_entity", new Dictionary + { + ["sceneId"] = sceneId, + ["entityId"] = entityId, + }); + } + + // ===================== + // UI Page Tools + // ===================== + + [McpIntegrationFact] + public async Task GetUITree_WithInvalidId_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("get_ui_tree", new Dictionary + { + ["assetId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + [McpIntegrationFact] + public async Task AddUIElement_CreatesAndRemovesElement() + { + // Create a UIPageAsset + var createAssetRoot = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "UIPageAsset", + ["name"] = "McpTestUIPage", + }); + + Assert.Null(createAssetRoot.GetProperty("error").GetString()); + var uiPageId = createAssetRoot.GetProperty("asset").GetProperty("id").GetString()!; + + // Verify tree is accessible (new UIPageAsset may have a default root element) + var treeRoot = await CallToolAndParseJsonAsync("get_ui_tree", new Dictionary + { + ["assetId"] = uiPageId, + }); + + Assert.Null(treeRoot.GetProperty("error").GetString()); + var uiPage = treeRoot.GetProperty("uiPage"); + var initialCount = uiPage.GetProperty("elementCount").GetInt32(); + + // Add a StackPanel as root + var addPanelRoot = await CallToolAndParseJsonAsync("add_ui_element", new Dictionary + { + ["assetId"] = uiPageId, + ["elementType"] = "StackPanel", + ["name"] = "MainPanel", + }); + + Assert.Null(addPanelRoot.GetProperty("error").GetString()); + var panelElement = addPanelRoot.GetProperty("element"); + Assert.Equal("StackPanel", panelElement.GetProperty("type").GetString()); + Assert.Equal("MainPanel", panelElement.GetProperty("name").GetString()); + var panelId = panelElement.GetProperty("id").GetString()!; + + // Add a TextBlock under the StackPanel + var addTextRoot = await CallToolAndParseJsonAsync("add_ui_element", new Dictionary + { + ["assetId"] = uiPageId, + ["elementType"] = "TextBlock", + ["name"] = "MyText", + ["parentId"] = panelId, + }); + + Assert.Null(addTextRoot.GetProperty("error").GetString()); + var textElement = addTextRoot.GetProperty("element"); + Assert.Equal("TextBlock", textElement.GetProperty("type").GetString()); + Assert.Equal(panelId, textElement.GetProperty("parentId").GetString()); + var textId = textElement.GetProperty("id").GetString()!; + + // Verify hierarchy via get_ui_tree + var treeAfterAdd = await CallToolAndParseJsonAsync("get_ui_tree", new Dictionary + { + ["assetId"] = uiPageId, + }); + + Assert.Null(treeAfterAdd.GetProperty("error").GetString()); + var pageAfterAdd = treeAfterAdd.GetProperty("uiPage"); + Assert.Equal(initialCount + 2, pageAfterAdd.GetProperty("elementCount").GetInt32()); + + // Find our StackPanel in the root elements and verify it has the TextBlock as a child + var rootElements = pageAfterAdd.GetProperty("elements"); + var panelNode = rootElements.EnumerateArray() + .FirstOrDefault(e => e.GetProperty("id").GetString() == panelId); + Assert.NotEqual(default, panelNode); + Assert.True(panelNode.GetProperty("children").GetArrayLength() > 0); + + // Inspect element via get_ui_element + var getElementRoot = await CallToolAndParseJsonAsync("get_ui_element", new Dictionary + { + ["assetId"] = uiPageId, + ["elementId"] = textId, + }); + + Assert.Null(getElementRoot.GetProperty("error").GetString()); + var detail = getElementRoot.GetProperty("element"); + Assert.Equal("TextBlock", detail.GetProperty("type").GetString()); + Assert.Equal(panelId, detail.GetProperty("parentId").GetString()); + Assert.True(detail.TryGetProperty("properties", out _)); + + // Remove the TextBlock + var removeRoot = await CallToolAndParseJsonAsync("remove_ui_element", new Dictionary + { + ["assetId"] = uiPageId, + ["elementId"] = textId, + }); + + Assert.Null(removeRoot.GetProperty("error").GetString()); + var removed = removeRoot.GetProperty("removed"); + Assert.Equal(textId, removed.GetProperty("id").GetString()); + + // Clean up: delete the UI page asset + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = uiPageId, + ["action"] = "delete", + }); + } + + [McpIntegrationFact] + public async Task SetUIElementProperty_SetsPropertyOnElement() + { + // Create a UIPageAsset + var createAssetRoot = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "UIPageAsset", + ["name"] = "McpTestUIPageProp", + }); + var uiPageId = createAssetRoot.GetProperty("asset").GetProperty("id").GetString()!; + + // Add a TextBlock + var addTextRoot = await CallToolAndParseJsonAsync("add_ui_element", new Dictionary + { + ["assetId"] = uiPageId, + ["elementType"] = "TextBlock", + ["name"] = "PropTest", + }); + var textId = addTextRoot.GetProperty("element").GetProperty("id").GetString()!; + + // Set width property + var setRoot = await CallToolAndParseJsonAsync("set_ui_element_property", new Dictionary + { + ["assetId"] = uiPageId, + ["elementId"] = textId, + ["propertyPath"] = "Width", + ["value"] = "200", + }); + + Assert.Null(setRoot.GetProperty("error").GetString()); + var setResult = setRoot.GetProperty("result"); + Assert.Equal("Width", setResult.GetProperty("propertyPath").GetString()); + + // Clean up + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = uiPageId, + ["action"] = "delete", + }); + } + + [McpIntegrationFact] + public async Task OpenUIPage_OpensAndCapturesViewport() + { + // Create a UIPageAsset + var createAssetRoot = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "UIPageAsset", + ["name"] = "McpTestUIPageViewport", + }); + + Assert.Null(createAssetRoot.GetProperty("error").GetString()); + var uiPageId = createAssetRoot.GetProperty("asset").GetProperty("id").GetString()!; + + // Open the UI page in the editor + var openRoot = await CallToolAndParseJsonAsync("open_ui_page", new Dictionary + { + ["assetId"] = uiPageId, + }); + + Assert.Equal("opened", openRoot.GetProperty("status").GetString()); + Assert.Equal("McpTestUIPageViewport", openRoot.GetProperty("name").GetString()); + + // Wait for the editor to initialize + await Task.Delay(3000); + + // Capture the viewport + var captureResult = await _client!.CallToolAsync("capture_viewport", new Dictionary + { + ["assetId"] = uiPageId, + }); + + // The result should contain either an image or a text error (editor may not be fully initialized) + Assert.NotNull(captureResult.Content); + Assert.True(captureResult.Content.Count > 0); + + // Clean up + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = uiPageId, + ["action"] = "delete", + }); + } + + [McpIntegrationFact] + public async Task OpenUIPage_WithInvalidId_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("open_ui_page", new Dictionary + { + ["assetId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + } + + // ===================== + // Sprite Frame Tools + // ===================== + + [McpIntegrationFact] + public async Task AddSpriteFrame_AddsAndRemovesFrame() + { + // Create a SpriteSheetAsset + var createAssetRoot = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "SpriteSheetAsset", + ["name"] = "McpTestSpriteSheet", + }); + + Assert.Null(createAssetRoot.GetProperty("error").GetString()); + var spriteSheetId = createAssetRoot.GetProperty("asset").GetProperty("id").GetString()!; + + // Add a sprite frame + var addRoot = await CallToolAndParseJsonAsync("add_sprite_frame", new Dictionary + { + ["assetId"] = spriteSheetId, + ["name"] = "Frame1", + }); + + Assert.Null(addRoot.GetProperty("error").GetString()); + var frame = addRoot.GetProperty("frame"); + Assert.Equal("Frame1", frame.GetProperty("name").GetString()); + Assert.Equal(0, frame.GetProperty("index").GetInt32()); + Assert.Equal(1, frame.GetProperty("totalFrames").GetInt32()); + + // Add another frame + var addRoot2 = await CallToolAndParseJsonAsync("add_sprite_frame", new Dictionary + { + ["assetId"] = spriteSheetId, + ["name"] = "Frame2", + ["textureRegionX"] = 0, + ["textureRegionY"] = 0, + ["textureRegionWidth"] = 64, + ["textureRegionHeight"] = 64, + }); + + Assert.Null(addRoot2.GetProperty("error").GetString()); + Assert.Equal(1, addRoot2.GetProperty("frame").GetProperty("index").GetInt32()); + Assert.Equal(2, addRoot2.GetProperty("frame").GetProperty("totalFrames").GetInt32()); + + // Remove the first frame + var removeRoot = await CallToolAndParseJsonAsync("remove_sprite_frame", new Dictionary + { + ["assetId"] = spriteSheetId, + ["index"] = 0, + }); + + Assert.Null(removeRoot.GetProperty("error").GetString()); + var removed = removeRoot.GetProperty("removed"); + Assert.Equal("Frame1", removed.GetProperty("name").GetString()); + Assert.Equal(1, removed.GetProperty("remainingCount").GetInt32()); + + // Clean up + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = spriteSheetId, + ["action"] = "delete", + }); + } + + [McpIntegrationFact] + public async Task RemoveSpriteFrame_WithInvalidIndex_ReturnsError() + { + // Create a SpriteSheetAsset + var createAssetRoot = await CallToolAndParseJsonAsync("create_asset", new Dictionary + { + ["assetType"] = "SpriteSheetAsset", + ["name"] = "McpTestSpriteSheetErr", + }); + var spriteSheetId = createAssetRoot.GetProperty("asset").GetProperty("id").GetString()!; + + // Try to remove from empty sprite sheet + var root = await CallToolAndParseJsonAsync("remove_sprite_frame", new Dictionary + { + ["assetId"] = spriteSheetId, + ["index"] = 0, + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + + // Clean up + await CallToolAndParseJsonAsync("manage_asset", new Dictionary + { + ["assetId"] = spriteSheetId, + ["action"] = "delete", + }); + } + + // ===================== + // Project Tools + // ===================== + + [McpIntegrationFact] + public async Task GetEditorStatus_IncludesProjects() + { + var root = await CallToolAndParseJsonAsync("get_editor_status"); + + Assert.True(root.TryGetProperty("projects", out var projects)); + Assert.True(projects.GetArrayLength() > 0, "Should have at least one project"); + + var firstProject = projects[0]; + Assert.False(string.IsNullOrEmpty(firstProject.GetProperty("name").GetString())); + Assert.False(string.IsNullOrEmpty(firstProject.GetProperty("type").GetString())); + Assert.False(string.IsNullOrEmpty(firstProject.GetProperty("platform").GetString())); + Assert.True(firstProject.TryGetProperty("isCurrentProject", out _)); + } + + [McpIntegrationFact] + public async Task SetActiveProject_WithInvalidName_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("set_active_project", new Dictionary + { + ["projectName"] = "NonExistentProject_99999", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + Assert.Contains("Available projects", root.GetProperty("error").GetString()!); + } + + [McpIntegrationFact] + public async Task SetActiveProject_ChangesActiveProject() + { + // Get current projects + var statusRoot = await CallToolAndParseJsonAsync("get_editor_status"); + var projects = statusRoot.GetProperty("projects"); + + if (projects.GetArrayLength() < 1) + { + // Skip if no projects available + return; + } + + // Set the first project as active (may already be active, but validates the command works) + var projectName = projects[0].GetProperty("name").GetString()!; + var root = await CallToolAndParseJsonAsync("set_active_project", new Dictionary + { + ["projectName"] = projectName, + }); + + Assert.Null(root.GetProperty("error").GetString()); + var project = root.GetProperty("project"); + Assert.Equal(projectName, project.GetProperty("name").GetString()); + Assert.True(project.GetProperty("isCurrentProject").GetBoolean()); + } + + // ===================== + // Root Assets + // ===================== + + [McpIntegrationFact] + public async Task ManageRootAssets_ListRootAssets() + { + var root = await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "list", + }); + + Assert.Null(root.GetProperty("error").GetString()); + Assert.True(root.TryGetProperty("assets", out var assets)); + // assets may be empty or non-empty depending on project state, but should be an array + Assert.Equal(JsonValueKind.Array, assets.ValueKind); + } + + [McpIntegrationFact] + public async Task ManageRootAssets_AddAndRemoveRootAsset() + { + // Use an existing scene from the project as the root asset candidate + var sceneId = await GetFirstSceneIdAsync(); + + // Check if it's already a root asset and remove it first if so + var initialList = await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "list", + }); + Assert.Null(initialList.GetProperty("error").GetString()); + var wasAlreadyRoot = initialList.GetProperty("assets").EnumerateArray() + .Any(a => a.GetProperty("id").GetString() == sceneId); + + if (wasAlreadyRoot) + { + await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "remove", + ["assetId"] = sceneId, + }); + } + + try + { + // Add as root + var addRoot = await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "add", + ["assetId"] = sceneId, + }); + Assert.Null(addRoot.GetProperty("error").GetString()); + Assert.Equal("added", addRoot.GetProperty("result").GetProperty("action").GetString()); + + // Verify it's in the list + var listRoot = await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "list", + }); + Assert.Null(listRoot.GetProperty("error").GetString()); + var assets = listRoot.GetProperty("assets"); + Assert.True(assets.EnumerateArray().Any(a => a.GetProperty("id").GetString() == sceneId), + "Newly added root asset should appear in list"); + + // Remove from root + var removeRoot = await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "remove", + ["assetId"] = sceneId, + }); + Assert.Null(removeRoot.GetProperty("error").GetString()); + Assert.Equal("removed", removeRoot.GetProperty("result").GetProperty("action").GetString()); + + // Verify it's gone from the list + var listAfterRemove = await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "list", + }); + Assert.Null(listAfterRemove.GetProperty("error").GetString()); + var assetsAfter = listAfterRemove.GetProperty("assets"); + Assert.False(assetsAfter.EnumerateArray().Any(a => a.GetProperty("id").GetString() == sceneId), + "Removed root asset should not appear in list"); + } + finally + { + // Restore original state if the asset was originally root + if (wasAlreadyRoot) + { + await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "add", + ["assetId"] = sceneId, + }); + } + } + } + + [McpIntegrationFact] + public async Task ManageRootAssets_AddNonExistentAsset_ReturnsError() + { + var root = await CallToolAndParseJsonAsync("manage_root_assets", new Dictionary + { + ["action"] = "add", + ["assetId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.False(string.IsNullOrEmpty(root.GetProperty("error").GetString())); + Assert.Contains("not found", root.GetProperty("error").GetString()!); + } + + [McpIntegrationFact] + public async Task GetEditorStatus_ProjectsIncludeTypeInfo() + { + var root = await CallToolAndParseJsonAsync("get_editor_status"); + + var projects = root.GetProperty("projects"); + Assert.True(projects.GetArrayLength() > 0, "Should have at least one project"); + + // Verify at least one project has isExecutable field + var hasIsExecutable = false; + foreach (var project in projects.EnumerateArray()) + { + if (project.TryGetProperty("isExecutable", out _)) + { + hasIsExecutable = true; + break; + } + } + Assert.True(hasIsExecutable, "At least one project should have isExecutable field"); + + // Verify rootAssetCount is present + Assert.True(root.TryGetProperty("rootAssetCount", out var rootAssetCount)); + Assert.True(rootAssetCount.GetInt32() >= 0); + } + + [McpIntegrationFact] + public async Task SetActiveProject_LibraryProjectWarning() + { + // Get projects and find a Library project + var statusRoot = await CallToolAndParseJsonAsync("get_editor_status"); + var projects = statusRoot.GetProperty("projects"); + + string? libraryProjectName = null; + string? originalProjectName = null; + foreach (var project in projects.EnumerateArray()) + { + if (project.GetProperty("isCurrentProject").GetBoolean()) + { + originalProjectName = project.GetProperty("name").GetString(); + } + if (project.GetProperty("type").GetString() == "Library") + { + libraryProjectName = project.GetProperty("name").GetString(); + } + } + + if (libraryProjectName == null) + { + // No Library project available to test — skip gracefully + return; + } + + // Set to Library project and verify warning + var root = await CallToolAndParseJsonAsync("set_active_project", new Dictionary + { + ["projectName"] = libraryProjectName, + }); + + Assert.Null(root.GetProperty("error").GetString()); + Assert.True(root.TryGetProperty("warning", out var warning)); + Assert.False(string.IsNullOrEmpty(warning.GetString())); + Assert.Contains("Library", warning.GetString()!); + + // Restore original project if we changed it + if (originalProjectName != null && originalProjectName != libraryProjectName) + { + await CallToolAndParseJsonAsync("set_active_project", new Dictionary + { + ["projectName"] = originalProjectName, + }); + } + } + + private async Task CallToolAndParseJsonAsync( + string toolName, + Dictionary? arguments = null) + { + var result = await _client!.CallToolAsync(toolName, arguments); + var textBlock = result.Content.OfType().First(); + var text = textBlock.Text!; + try + { + var doc = JsonDocument.Parse(text); + return doc.RootElement; + } + catch (JsonException ex) + { + throw new JsonException($"Failed to parse response from '{toolName}' as JSON: {text}", ex); + } + } + + private async Task GetFirstSceneIdAsync() + { + var root = await CallToolAndParseJsonAsync("get_editor_status"); + var scenes = root.GetProperty("scenes"); + Assert.True(scenes.GetArrayLength() > 0, "No scenes found in project"); + return scenes[0].GetProperty("id").GetString()!; + } + + private async Task GetFirstEntityIdAsync(string sceneId) + { + var root = await CallToolAndParseJsonAsync("get_scene_tree", new Dictionary + { + ["sceneId"] = sceneId, + }); + var entities = root.GetProperty("scene").GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0, "No entities found in scene"); + return entities[0].GetProperty("id").GetString()!; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp.Tests/README.md b/sources/editor/Stride.GameStudio.Mcp.Tests/README.md new file mode 100644 index 0000000000..ee68983df3 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp.Tests/README.md @@ -0,0 +1,73 @@ +# MCP Integration Tests + +Integration tests for the MCP server embedded in Stride Game Studio. These tests verify all MCP tools work correctly by connecting to a live Game Studio instance via the MCP client SDK. + +Tests are **disabled by default** and must be explicitly opted in, because they require a desktop environment and a pre-built Game Studio. + +## Prerequisites + +Game Studio must be built before running the tests. The build process requires Visual Studio MSBuild (not `dotnet build`) because of transitive native C++ dependencies, and a NuGet packaging workaround for .NET 10. + +A bootstrap script automates all of this: + +```powershell +.\sources\editor\Stride.GameStudio.Mcp.Tests\bootstrap.ps1 +``` + +The script will: +1. Locate Visual Studio MSBuild via `vswhere` +2. Build Game Studio with `StrideSkipAutoPack=true` +3. Copy pruned framework DLLs to the build output (workaround for a .NET 10 issue where `Microsoft.Extensions.FileProviders.Abstractions.dll` and related DLLs are missing) +4. Pack the `Stride.GameStudio` NuGet package to the local dev feed (`%LOCALAPPDATA%\Stride\NugetDev`) +5. Build the integration test project + +## Running the Tests + +```powershell +$env:STRIDE_MCP_INTEGRATION_TESTS = "true" +dotnet test sources\editor\Stride.GameStudio.Mcp.Tests +``` + +The test fixture automatically: +1. Launches Game Studio with the FirstPersonShooter sample project +2. Polls the MCP SSE endpoint until the server is ready (up to 120 seconds) +3. Runs all 8 integration tests +4. Kills the Game Studio process + +## Configuration + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `STRIDE_MCP_INTEGRATION_TESTS` | *(unset)* | Set to `true` to enable tests. When unset, all tests are skipped. | +| `STRIDE_MCP_PORT` | `5271` | Port for the MCP server | +| `STRIDE_GAMESTUDIO_EXE` | *(auto-detected)* | Override path to `Stride.GameStudio.exe` | +| `STRIDE_TEST_PROJECT` | *(auto-detected)* | Override path to the `.sln` file to open | + +## What the Tests Cover + +| Test | Description | +|------|-------------| +| `ListTools_ReturnsAllExpectedTools` | All 4 tools are registered | +| `GetEditorStatus_ReturnsProjectInfo` | Returns project name, solution path, asset count, scenes | +| `QueryAssets_ReturnsAssets` | Returns asset list with id/name/type metadata | +| `QueryAssets_WithTypeFilter_ReturnsOnlyMatchingType` | Type filter works correctly | +| `GetSceneTree_ReturnsEntityHierarchy` | Returns entity hierarchy with components and children | +| `GetSceneTree_WithInvalidId_ReturnsError` | Graceful error for invalid scene ID | +| `GetEntity_ReturnsComponentDetails` | Returns component properties including TransformComponent | +| `GetEntity_WithInvalidEntityId_ReturnsError` | Graceful error for invalid entity ID | + +## Troubleshooting + +**Tests skip with "Set STRIDE_MCP_INTEGRATION_TESTS=true"** +- Set the environment variable: `$env:STRIDE_MCP_INTEGRATION_TESTS = "true"` + +**"Stride.GameStudio.exe not found"** +- Run `bootstrap.ps1` to build Game Studio, or set `STRIDE_GAMESTUDIO_EXE` to an existing build + +**"GameStudio exited prematurely"** +- Check that the `Stride.GameStudio` NuGet package exists in `%LOCALAPPDATA%\Stride\NugetDev\` +- Re-run `bootstrap.ps1` to rebuild and repack + +**"MCP server did not become ready within 120s"** +- Game Studio may be stuck during project loading. Check the captured stdout/stderr in the error message. +- Ensure no other Game Studio instance is already using port 5271 diff --git a/sources/editor/Stride.GameStudio.Mcp.Tests/Stride.GameStudio.Mcp.Tests.csproj b/sources/editor/Stride.GameStudio.Mcp.Tests/Stride.GameStudio.Mcp.Tests.csproj new file mode 100644 index 0000000000..22e47156e8 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp.Tests/Stride.GameStudio.Mcp.Tests.csproj @@ -0,0 +1,19 @@ + + + net10.0 + win-x64 + enable + enable + true + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/sources/editor/Stride.GameStudio.Mcp.Tests/bootstrap.ps1 b/sources/editor/Stride.GameStudio.Mcp.Tests/bootstrap.ps1 new file mode 100644 index 0000000000..26b2dcdcaf --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp.Tests/bootstrap.ps1 @@ -0,0 +1,183 @@ +<# +.SYNOPSIS + Builds Stride Game Studio and prepares the environment for MCP integration tests. + +.DESCRIPTION + This script automates the complex build pipeline required before running MCP + integration tests. It must be run once (or after code changes) before tests. + + Steps performed: + 1. Locate Visual Studio MSBuild (required - dotnet build cannot handle native C++ deps) + 2. Build Game Studio with StrideSkipAutoPack=true + 3. Copy missing pruned framework DLLs to build output (workaround for .NET 10 issue) + 4. Pack the Stride.GameStudio NuGet package to the local dev feed + 5. Build the integration test project + +.EXAMPLE + .\bootstrap.ps1 + .\bootstrap.ps1 -Configuration Release +#> + +param( + [ValidateSet("Debug", "Release")] + [string]$Configuration = "Debug" +) + +$ErrorActionPreference = "Stop" +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") + +Write-Host "=== MCP Integration Test Bootstrap ===" -ForegroundColor Cyan +Write-Host "Repository root: $RepoRoot" +Write-Host "Configuration: $Configuration" +Write-Host "" + +# --- Step 1: Locate MSBuild --- +Write-Host "[1/5] Locating Visual Studio MSBuild..." -ForegroundColor Yellow + +$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +if (Test-Path $vswhere) { + $vsPath = & $vswhere -latest -requires Microsoft.Component.MSBuild -property installationPath 2>$null + if ($vsPath) { + $MSBuild = Join-Path $vsPath "MSBuild\Current\Bin\MSBuild.exe" + } +} + +if (-not $MSBuild -or -not (Test-Path $MSBuild)) { + # Fallback to known path + $MSBuild = "C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe" +} + +if (-not (Test-Path $MSBuild)) { + Write-Error @" +Could not locate Visual Studio MSBuild. +Game Studio requires VS MSBuild (not 'dotnet build') because it transitively +depends on native C++ vcxproj files. + +Please install Visual Studio with the 'Desktop development with C++' workload. +"@ + exit 1 +} + +Write-Host " Found: $MSBuild" -ForegroundColor Green + +# --- Step 2: Build Game Studio --- +Write-Host "" +Write-Host "[2/5] Building Game Studio ($Configuration)..." -ForegroundColor Yellow + +$GameStudioCsproj = Join-Path $RepoRoot "sources\editor\Stride.GameStudio\Stride.GameStudio.csproj" + +& $MSBuild $GameStudioCsproj ` + "-p:Configuration=$Configuration" ` + "-p:StrideSkipAutoPack=true" ` + "-verbosity:quiet" ` + "-m" + +if ($LASTEXITCODE -ne 0) { + Write-Error "Game Studio build failed with exit code $LASTEXITCODE" + exit 1 +} + +$BuildOutput = Join-Path $RepoRoot "sources\editor\Stride.GameStudio\bin\$Configuration\net10.0-windows" +$GameStudioExe = Join-Path $BuildOutput "Stride.GameStudio.exe" + +if (-not (Test-Path $GameStudioExe)) { + Write-Error "Build succeeded but Stride.GameStudio.exe not found at: $GameStudioExe" + exit 1 +} + +Write-Host " Build succeeded: $GameStudioExe" -ForegroundColor Green + +# --- Step 3: Copy pruned framework DLLs --- +Write-Host "" +Write-Host "[3/5] Copying pruned framework DLLs to build output..." -ForegroundColor Yellow + +# These DLLs are explicitly referenced by Stride.NuGetResolver.Targets.projitems +# (lines 38-40) for inclusion in the NuGet package. On .NET 10, they may be pruned +# from the build output because they're part of the shared framework. +$PrunedDlls = @( + "Microsoft.Extensions.FileProviders.Abstractions.dll", + "Microsoft.Extensions.FileSystemGlobbing.dll", + "Microsoft.Extensions.Primitives.dll" +) + +# Find the ASP.NET Core shared framework directory +$AspNetFrameworkDir = Get-ChildItem "$env:ProgramFiles\dotnet\shared\Microsoft.AspNetCore.App" -Directory | + Sort-Object { [Version]$_.Name } -ErrorAction SilentlyContinue | + Select-Object -Last 1 + +if (-not $AspNetFrameworkDir) { + Write-Warning "Could not locate ASP.NET Core shared framework. NuGet pack may fail." +} else { + $CopiedCount = 0 + foreach ($dll in $PrunedDlls) { + $target = Join-Path $BuildOutput $dll + if (-not (Test-Path $target)) { + $source = Join-Path $AspNetFrameworkDir.FullName $dll + if (Test-Path $source) { + Copy-Item $source $target + Write-Host " Copied: $dll" -ForegroundColor Green + $CopiedCount++ + } else { + Write-Warning " Source not found: $source" + } + } else { + Write-Host " Already exists: $dll" -ForegroundColor DarkGray + } + } + if ($CopiedCount -eq 0) { + Write-Host " All DLLs already present." -ForegroundColor Green + } +} + +# --- Step 4: Pack NuGet package --- +Write-Host "" +Write-Host "[4/5] Packing Stride.GameStudio NuGet package..." -ForegroundColor Yellow + +& $MSBuild $GameStudioCsproj ` + "-t:Pack" ` + "-p:Configuration=$Configuration" ` + "-p:StrideSkipAutoPack=true" ` + "-verbosity:quiet" ` + "-m" + +if ($LASTEXITCODE -ne 0) { + Write-Error @" +NuGet pack failed with exit code $LASTEXITCODE. +This is a known issue on .NET 10 when pruned framework DLLs are missing. +Check that Step 3 copied the required DLLs successfully. +"@ + exit 1 +} + +# Verify the package was deployed to the dev feed +$DevFeed = Join-Path $env:LOCALAPPDATA "Stride\NugetDev" +$PackageExists = Get-ChildItem "$DevFeed\Stride.GameStudio.*.nupkg" -ErrorAction SilentlyContinue | Select-Object -First 1 +if ($PackageExists) { + Write-Host " Package deployed to: $($PackageExists.FullName)" -ForegroundColor Green +} else { + Write-Warning " Package not found in dev feed at: $DevFeed" + Write-Warning " Game Studio may fail to open projects." +} + +# --- Step 5: Build test project --- +Write-Host "" +Write-Host "[5/5] Building integration test project..." -ForegroundColor Yellow + +$TestCsproj = Join-Path $RepoRoot "sources\editor\Stride.GameStudio.Mcp.Tests\Stride.GameStudio.Mcp.Tests.csproj" +dotnet build $TestCsproj --verbosity quiet + +if ($LASTEXITCODE -ne 0) { + Write-Error "Test project build failed with exit code $LASTEXITCODE" + exit 1 +} + +Write-Host " Build succeeded." -ForegroundColor Green + +# --- Done --- +Write-Host "" +Write-Host "=== Bootstrap complete! ===" -ForegroundColor Cyan +Write-Host "" +Write-Host "To run the integration tests:" -ForegroundColor White +Write-Host ' $env:STRIDE_MCP_INTEGRATION_TESTS = "true"' -ForegroundColor White +Write-Host " dotnet test $TestCsproj" -ForegroundColor White +Write-Host "" diff --git a/sources/editor/Stride.GameStudio.Mcp/DispatcherBridge.cs b/sources/editor/Stride.GameStudio.Mcp/DispatcherBridge.cs new file mode 100644 index 0000000000..0e2bf758dd --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/DispatcherBridge.cs @@ -0,0 +1,218 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Stride.Core.Presentation.Dialogs; +using Stride.Core.Presentation.Services; + +namespace Stride.GameStudio.Mcp; + +/// +/// Bridges MCP tool handler threads (background) to the WPF dispatcher thread +/// where all editor state must be accessed. +/// +public sealed class DispatcherBridge +{ + private readonly IDispatcherService _dispatcher; + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + + /// + /// Recent dialog messages that were suppressed during MCP execution. + /// Visible via get_editor_status so agents can see dialog errors. + /// + public static ConcurrentQueue RecentSuppressedDialogs { get; } = new(); + + private const int MaxRecentDialogs = 20; + + public DispatcherBridge(IDispatcherService dispatcher) + { + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + /// + /// Executes an action on the UI thread and returns the result. + /// This is the primary way MCP tools access editor state. + /// + public async Task InvokeOnUIThread(Func action, CancellationToken cancellationToken = default) + { + if (_dispatcher.CheckAccess()) + { + return WithDialogSuppression(action); + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(DefaultTimeout); + + return await _dispatcher.InvokeAsync(() => WithDialogSuppression(action), cts.Token); + } + + /// + /// Executes an action on the UI thread without a return value. + /// + public async Task InvokeOnUIThread(Action action, CancellationToken cancellationToken = default) + { + if (_dispatcher.CheckAccess()) + { + WithDialogSuppression(action); + return; + } + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(DefaultTimeout); + + await _dispatcher.InvokeAsync(() => WithDialogSuppression(action), cts.Token); + } + + /// + /// Executes an async task on the UI thread and returns the result. + /// Uses a custom TCS to guarantee exception propagation and timeout — + /// the built-in DispatcherService.InvokeTask swallows exceptions and hangs forever. + /// + public async Task InvokeTaskOnUIThread(Func> task, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(DefaultTimeout); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var registration = cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)); + + // Schedule the async work on the dispatcher thread. + // InvokeAsync completes once the synchronous part runs (the fire-and-forget kick-off). + // The TCS is completed by the async continuation with result, cancellation, or exception. + await _dispatcher.InvokeAsync(() => + { + _ = ExecuteAndComplete(() => WithDialogSuppressionAsync(task), tcs); + }, cts.Token); + + return await tcs.Task; + } + + /// + /// Executes an async task on the UI thread without a return value. + /// Uses a custom TCS to guarantee exception propagation and timeout. + /// + public async Task InvokeTaskOnUIThread(Func task, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(DefaultTimeout); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var registration = cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)); + + await _dispatcher.InvokeAsync(() => + { + _ = ExecuteAndComplete(async () => + { + await WithDialogSuppressionAsync(task); + return true; + }, tcs); + }, cts.Token); + + await tcs.Task; + } + + /// + /// Runs an async function and completes the TCS with result, cancellation, or exception. + /// Guaranteed to set the TCS in all code paths. + /// + private static async Task ExecuteAndComplete(Func> task, TaskCompletionSource tcs) + { + try + { + tcs.TrySetResult(await task()); + } + catch (OperationCanceledException ex) + { + tcs.TrySetCanceled(ex.CancellationToken); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + } + + private static T WithDialogSuppression(Func action) + { + DialogService.SuppressDialogs = true; + DialogService.SuppressedDialogMessages = new List(); + try + { + var result = action(); + ThrowIfDialogsSuppressed(); + return result; + } + finally + { + DialogService.SuppressDialogs = false; + DialogService.SuppressedDialogMessages = null; + } + } + + private static void WithDialogSuppression(Action action) + { + DialogService.SuppressDialogs = true; + DialogService.SuppressedDialogMessages = new List(); + try + { + action(); + ThrowIfDialogsSuppressed(); + } + finally + { + DialogService.SuppressDialogs = false; + DialogService.SuppressedDialogMessages = null; + } + } + + private static async Task WithDialogSuppressionAsync(Func> task) + { + DialogService.SuppressDialogs = true; + DialogService.SuppressedDialogMessages = new List(); + try + { + var result = await task(); + ThrowIfDialogsSuppressed(); + return result; + } + finally + { + DialogService.SuppressDialogs = false; + DialogService.SuppressedDialogMessages = null; + } + } + + private static async Task WithDialogSuppressionAsync(Func task) + { + DialogService.SuppressDialogs = true; + DialogService.SuppressedDialogMessages = new List(); + try + { + await task(); + ThrowIfDialogsSuppressed(); + } + finally + { + DialogService.SuppressDialogs = false; + DialogService.SuppressedDialogMessages = null; + } + } + + private static void ThrowIfDialogsSuppressed() + { + var msgs = DialogService.SuppressedDialogMessages; + if (msgs is { Count: > 0 }) + { + foreach (var msg in msgs) + { + RecentSuppressedDialogs.Enqueue(msg); + while (RecentSuppressedDialogs.Count > MaxRecentDialogs) + RecentSuppressedDialogs.TryDequeue(out _); + } + throw new McpDialogSuppressedException(msgs); + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/McpConfigFile.cs b/sources/editor/Stride.GameStudio.Mcp/McpConfigFile.cs new file mode 100644 index 0000000000..9add25f22b --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/McpConfigFile.cs @@ -0,0 +1,180 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using Stride.Core.Diagnostics; + +namespace Stride.GameStudio.Mcp; + +/// +/// JSON data model for .stride/mcp.json at the solution root. +/// Serves as both user configuration and runtime discovery for AI agents. +/// +public sealed class McpConfig +{ + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("runtime")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public McpRuntimeInfo? Runtime { get; set; } +} + +/// +/// Runtime information written by the MCP server on start, cleared on stop. +/// Allows AI agents to discover the actual port and check PID liveness. +/// +public sealed class McpRuntimeInfo +{ + [JsonPropertyName("actualPort")] + public int ActualPort { get; set; } + + [JsonPropertyName("pid")] + public int Pid { get; set; } + + [JsonPropertyName("startedAt")] + public DateTimeOffset StartedAt { get; set; } +} + +/// +/// File I/O helpers for reading and writing .stride/mcp.json. +/// All I/O is wrapped in try-catch so file issues never prevent the editor from starting. +/// +public static class McpConfigFile +{ + private static readonly Logger Log = GlobalLogger.GetLogger("McpConfig"); + private const string DirectoryName = ".stride"; + private const string FileName = "mcp.json"; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + }; + + /// + /// Returns the full path to .stride/mcp.json for the given solution directory. + /// + public static string GetConfigPath(string solutionDir) + => Path.Combine(solutionDir, DirectoryName, FileName); + + /// + /// Reads and deserializes the config file. Returns defaults if missing or malformed. + /// + public static McpConfig Load(string? solutionDir) + { + if (string.IsNullOrEmpty(solutionDir)) + return new McpConfig(); + + try + { + var path = GetConfigPath(solutionDir); + if (!File.Exists(path)) + return new McpConfig(); + + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json, JsonOptions) ?? new McpConfig(); + } + catch (Exception ex) + { + Log.Warning($"Failed to read MCP config: {ex.Message}"); + return new McpConfig(); + } + } + + /// + /// Creates .stride/mcp.json with default settings if it does not already exist. + /// Called on every editor launch so users can discover and edit the file. + /// + public static void EnsureExists(string? solutionDir) + { + if (string.IsNullOrEmpty(solutionDir)) + return; + + try + { + var path = GetConfigPath(solutionDir); + if (File.Exists(path)) + return; + + Save(solutionDir, new McpConfig()); + Log.Info($"Created default MCP config at {path}. Set \"enabled\": true to activate."); + } + catch (Exception ex) + { + Log.Warning($"Failed to create default MCP config: {ex.Message}"); + } + } + + /// + /// Serializes and writes the config file, creating the .stride/ directory if needed. + /// + public static void Save(string? solutionDir, McpConfig config) + { + if (string.IsNullOrEmpty(solutionDir)) + return; + + try + { + var dirPath = Path.Combine(solutionDir, DirectoryName); + Directory.CreateDirectory(dirPath); + + var path = Path.Combine(dirPath, FileName); + var json = JsonSerializer.Serialize(config, JsonOptions); + File.WriteAllText(path, json); + } + catch (Exception ex) + { + Log.Warning($"Failed to write MCP config: {ex.Message}"); + } + } + + /// + /// Loads the config, sets runtime info (actual port, PID, timestamp), and saves. + /// + public static void WriteRuntimeInfo(string? solutionDir, int port, int pid) + { + if (string.IsNullOrEmpty(solutionDir)) + return; + + try + { + var config = Load(solutionDir); + config.Runtime = new McpRuntimeInfo + { + ActualPort = port, + Pid = pid, + StartedAt = DateTimeOffset.UtcNow, + }; + Save(solutionDir, config); + } + catch (Exception ex) + { + Log.Warning($"Failed to write MCP runtime info: {ex.Message}"); + } + } + + /// + /// Loads the config, clears runtime info, and saves. + /// + public static void ClearRuntimeInfo(string? solutionDir) + { + if (string.IsNullOrEmpty(solutionDir)) + return; + + try + { + var config = Load(solutionDir); + config.Runtime = null; + Save(solutionDir, config); + } + catch (Exception ex) + { + Log.Warning($"Failed to clear MCP runtime info: {ex.Message}"); + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/McpDialogSuppressedException.cs b/sources/editor/Stride.GameStudio.Mcp/McpDialogSuppressedException.cs new file mode 100644 index 0000000000..d01f4ff1bf --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/McpDialogSuppressedException.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.GameStudio.Mcp; + +/// +/// Thrown when dialog(s) were suppressed during MCP tool execution. +/// Carries the dialog messages so they can be reported back to the agent as errors. +/// +public sealed class McpDialogSuppressedException : Exception +{ + public IReadOnlyList DialogMessages { get; } + + public McpDialogSuppressedException(IReadOnlyList dialogMessages) + : base($"Editor showed {dialogMessages.Count} dialog(s) during MCP execution: {string.Join(" | ", dialogMessages)}") + { + DialogMessages = dialogMessages; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/McpEditorPlugin.cs b/sources/editor/Stride.GameStudio.Mcp/McpEditorPlugin.cs new file mode 100644 index 0000000000..1522fe3eb3 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/McpEditorPlugin.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Diagnostics; +using Stride.Editor; + +namespace Stride.GameStudio.Mcp; + +/// +/// MCP (Model Context Protocol) plugin for Stride Game Studio. +/// Hosts an HTTP/SSE MCP server that exposes editor functionality as tools +/// for LLM agents to interact with. +/// +public sealed class McpEditorPlugin : StrideAssetsPlugin +{ + private static readonly Logger Log = GlobalLogger.GetLogger("McpPlugin"); + + protected override void Initialize(ILogger logger) + { + // No static initialization needed + } + + public override void InitializeSession(SessionViewModel session) + { + try + { + var solutionDir = ResolveSolutionDirectory(session); + McpConfigFile.EnsureExists(solutionDir); + var mcpService = new McpServerService(session, solutionDir); + session.ServiceProvider.RegisterService(mcpService); + mcpService.StartAsync().ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error("Failed to start MCP server", t.Exception?.InnerException ?? t.Exception); + } + }); + } + catch (Exception ex) + { + Log.Error("Failed to initialize MCP plugin", ex); + } + } + + protected override void SessionDisposed(SessionViewModel session) + { + try + { + var mcpService = session.ServiceProvider.TryGet(); + mcpService?.StopAsync().Wait(TimeSpan.FromSeconds(5)); + } + catch (Exception ex) + { + Log.Error("Error stopping MCP server", ex); + } + + base.SessionDisposed(session); + } + + public override void RegisterAssetPreviewViewTypes(IDictionary assetPreviewViewTypes) + { + // No preview types to register + } + + /// + /// Resolves the solution root directory from the session's solution path. + /// Returns null if no solution path is available. + /// + private static string? ResolveSolutionDirectory(SessionViewModel session) + { + var solutionPath = session.SolutionPath; + if (solutionPath == null) + return null; + + return solutionPath.GetFullDirectory()?.ToOSPath(); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/McpServerService.cs b/sources/editor/Stride.GameStudio.Mcp/McpServerService.cs new file mode 100644 index 0000000000..f746664d8c --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/McpServerService.cs @@ -0,0 +1,229 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Server; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Diagnostics; +using Stride.Core.Presentation.Services; + +namespace Stride.GameStudio.Mcp; + +/// +/// Hosts an MCP server inside GameStudio using Kestrel with SSE transport. +/// The server exposes editor functionality as MCP tools that LLM agents can call. +/// +public sealed class McpServerService : IDisposable +{ + private static readonly Logger Log = GlobalLogger.GetLogger("McpServer"); + private const int DefaultPort = 5271; + private const int MaxPortRetries = 10; + + private readonly SessionViewModel _session; + private readonly string? _solutionDir; + private readonly DispatcherBridge _dispatcherBridge; + private WebApplication? _webApp; + private CancellationTokenSource? _cts; + + public int Port { get; private set; } + public bool IsRunning => _webApp != null; + + public McpServerService(SessionViewModel session, string? solutionDir) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + _solutionDir = solutionDir; + + var dispatcher = session.ServiceProvider.Get(); + _dispatcherBridge = new DispatcherBridge(dispatcher); + } + + /// + /// Determines whether the MCP server should start. + /// Priority: env var > .stride/mcp.json > default (false). + /// + private bool IsEnabled() + { + // Env var takes highest priority (for CI, tests, and command-line override) + var envEnabled = Environment.GetEnvironmentVariable("STRIDE_MCP_ENABLED"); + if (envEnabled != null) + return !string.Equals(envEnabled, "false", StringComparison.OrdinalIgnoreCase); + + // Fall back to .stride/mcp.json (default: false) + return McpConfigFile.Load(_solutionDir).Enabled; + } + + /// + /// Resolves the port to use. + /// Priority: env var > .stride/mcp.json > auto-select. + /// A value of 0 means auto-select starting from DefaultPort. + /// + private int ResolveConfiguredPort() + { + // Env var takes highest priority + var portStr = Environment.GetEnvironmentVariable("STRIDE_MCP_PORT"); + if (int.TryParse(portStr, out var envPort)) + return envPort; + + // Fall back to .stride/mcp.json (default: 0 = auto) + return McpConfigFile.Load(_solutionDir).Port; + } + + public async Task StartAsync() + { + if (!IsEnabled()) + { + Log.Info("MCP server is disabled. Create .stride/mcp.json with {\"enabled\": true} or set STRIDE_MCP_ENABLED=true."); + return; + } + + _cts = new CancellationTokenSource(); + + var configuredPort = ResolveConfiguredPort(); + if (configuredPort > 0) + { + // Fixed port requested — try it once + await TryStartOnPort(configuredPort); + } + else + { + // Auto-select: try DefaultPort, then increment + var started = false; + for (int i = 0; i < MaxPortRetries; i++) + { + var candidatePort = DefaultPort + i; + if (!IsPortAvailable(candidatePort)) + { + Log.Info($"Port {candidatePort} is in use, trying next..."); + continue; + } + + try + { + await TryStartOnPort(candidatePort); + started = true; + break; + } + catch (IOException) + { + // Port may have been taken between check and bind — try next + Log.Info($"Port {candidatePort} could not be bound, trying next..."); + } + } + + if (!started) + { + Log.Error($"Failed to start MCP server: could not find an available port in range {DefaultPort}-{DefaultPort + MaxPortRetries - 1}"); + _cts?.Dispose(); + _cts = null; + } + } + } + + private async Task TryStartOnPort(int port) + { + try + { + var builder = WebApplication.CreateSlimBuilder(); + builder.WebHost.ConfigureKestrel(options => + { + options.ListenLocalhost(port); + }); + + // Suppress ASP.NET Core console logging to avoid polluting GameStudio output + builder.Logging.ClearProviders(); + builder.Logging.SetMinimumLevel(LogLevel.Warning); + + // Register our services for DI into tool methods + builder.Services.AddSingleton(_session); + builder.Services.AddSingleton(_dispatcherBridge); + + builder.Services + .AddMcpServer(options => + { + options.ServerInfo = new() + { + Name = "Stride Game Studio", + Version = typeof(McpServerService).Assembly.GetName().Version?.ToString() ?? "0.1.0", + }; + }) + .WithHttpTransport() + .WithToolsFromAssembly(typeof(McpServerService).Assembly); + + _webApp = builder.Build(); + _webApp.MapMcp(); + + Log.Info($"MCP server starting on http://localhost:{port}/sse"); + await _webApp.StartAsync(_cts!.Token); + Port = port; + Log.Info($"MCP server started successfully on http://localhost:{port}/sse"); + + // Write runtime info for agent discovery + var pid = Process.GetCurrentProcess().Id; + McpConfigFile.WriteRuntimeInfo(_solutionDir, port, pid); + } + catch (Exception ex) + { + Log.Error($"Failed to start MCP server on port {port}", ex); + _webApp = null; + throw; + } + } + + private static bool IsPortAvailable(int port) + { + try + { + using var listener = new TcpListener(IPAddress.Loopback, port); + listener.Start(); + listener.Stop(); + return true; + } + catch (SocketException) + { + return false; + } + } + + public async Task StopAsync() + { + if (_webApp == null) + return; + + Log.Info("MCP server stopping..."); + try + { + using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await _webApp.StopAsync(stopCts.Token); + await _webApp.DisposeAsync(); + Log.Info("MCP server stopped"); + } + catch (Exception ex) + { + Log.Error("Error stopping MCP server", ex); + } + finally + { + _webApp = null; + _cts?.Dispose(); + _cts = null; + + // Clear runtime info so agents know the server is gone + McpConfigFile.ClearRuntimeInfo(_solutionDir); + } + } + + public void Dispose() + { + StopAsync().Wait(TimeSpan.FromSeconds(5)); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/README.md b/sources/editor/Stride.GameStudio.Mcp/README.md new file mode 100644 index 0000000000..151cca3c87 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/README.md @@ -0,0 +1,272 @@ +# Stride Game Studio MCP Server + +An embedded [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that exposes Stride Game Studio editor functionality as tools for AI agents. This allows LLM-powered coding assistants to query project structure, browse scenes, and inspect entities directly from a running editor instance. + +## How It Works + +When Game Studio launches and opens a project, the MCP plugin automatically starts an HTTP server on `localhost:5271` using Server-Sent Events (SSE) transport. Any MCP-compatible client can connect to this endpoint and call tools to interact with the editor. + +## Available Tools + +### State Reading +| Tool | Description | +|------|-------------| +| `get_editor_status` | Returns project name, solution path, packages, projects (with type/platform/active status), asset count, and scene listing | +| `query_assets` | Search and filter assets by name, type, or folder path | +| `get_scene_tree` | Returns the full entity hierarchy for a scene | +| `get_entity` | Returns detailed component and property data for an entity | + +### Navigation & Selection +| Tool | Description | +|------|-------------| +| `open_scene` | Opens a scene asset in the editor (or activates if already open) | +| `open_ui_page` | Opens a UI page asset in the editor (or activates if already open) | +| `select_entity` | Selects entities in the scene editor hierarchy (supports multi-select) | +| `focus_entity` | Centers the viewport camera on an entity (also selects it) | + +### Modification +| Tool | Description | +|------|-------------| +| `create_entity` | Creates a new entity in a scene (with optional parent) | +| `delete_entity` | Deletes an entity and its children from a scene | +| `reparent_entity` | Moves an entity to a new parent (or to root level) | +| `set_transform` | Sets position, rotation (Euler degrees), and/or scale of an entity | +| `modify_component` | Adds, removes, or updates a component on an entity | + +### Asset Management +| Tool | Description | +|------|-------------| +| `get_asset_details` | Returns detailed properties, source file, archetype, and tags for an asset | +| `get_asset_dependencies` | Shows what references an asset (inbound) and what it references (outbound) | +| `create_asset` | Creates a new asset (material, prefab, scene, etc.) with defaults | +| `manage_asset` | Renames, moves, or deletes an asset (with reference safety checks) | +| `set_asset_property` | Sets a property on an asset via dot-notation path through the property graph | +| `add_sprite_frame` | Adds a new sprite frame to a SpriteSheetAsset | +| `remove_sprite_frame` | Removes a sprite frame from a SpriteSheetAsset by index | +| `manage_root_assets` | Manages which assets are included in the game build (list/add/remove root assets) | + +### UI Pages +| Tool | Description | +|------|-------------| +| `get_ui_tree` | Returns the full UI element hierarchy for a UIPageAsset | +| `get_ui_element` | Returns detailed properties for a specific UI element | +| `add_ui_element` | Creates a new UI element (Button, TextBlock, StackPanel, etc.) in a page | +| `remove_ui_element` | Removes a UI element and its descendants from a page | +| `set_ui_element_property` | Sets a property on a UI element via dot-notation path | + +### Project +| Tool | Description | +|------|-------------| +| `save_project` | Saves all changes (scenes, entities, assets, etc.) to disk | +| `reload_scene` | Closes and reopens a scene editor tab to refresh its state | +| `restart_game_studio` | Restarts Game Studio entirely — use only for .csproj/.sln changes (drops MCP connection) | +| `set_active_project` | Changes which project is active — select an Executable project for builds (warns if Library is selected) | + +### Viewport +| Tool | Description | +|------|-------------| +| `capture_viewport` | Captures a PNG screenshot of the viewport for an open scene or UI page | + +### Build +| Tool | Description | +|------|-------------| +| `build_project` | Triggers an async build of the current game project | +| `get_build_status` | Returns current build status, errors, and warnings | +| `reload_assemblies` | Reloads game assemblies after a build to make user script types available | + +## Configuration + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `STRIDE_MCP_PORT` | `5271` | Port for the MCP HTTP/SSE server | +| `STRIDE_MCP_ENABLED` | `true` | Set to `false` to disable the MCP server | + +## Connecting AI Agents + +### Claude Code (CLI) + +Add to your project's `.mcp.json` file or `~/.claude.json`: + +```json +{ + "mcpServers": { + "stride-editor": { + "type": "sse", + "url": "http://localhost:5271/sse" + } + } +} +``` + +Then start Claude Code. It will automatically connect to Game Studio when it's running. + +### Claude Desktop + +Add to your Claude Desktop config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows): + +```json +{ + "mcpServers": { + "stride-editor": { + "type": "sse", + "url": "http://localhost:5271/sse" + } + } +} +``` + +Restart Claude Desktop after editing the config. + +### Continue (VS Code / JetBrains) + +Add to your Continue config file (`.continue/config.yaml`): + +```yaml +mcpServers: + - name: stride-editor + url: http://localhost:5271/sse +``` + +See [Continue MCP docs](https://docs.continue.dev/customize/context-providers#mcp) for details. + +### JetBrains Junie + +Add to your project's `.junie/mcp.json`: + +```json +{ + "mcpServers": { + "stride-editor": { + "type": "sse", + "url": "http://localhost:5271/sse" + } + } +} +``` + +See [Junie MCP docs](https://www.jetbrains.com/help/junie/mcp-servers.html) for details. + +### Cursor + +Add to your project's `.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "stride-editor": { + "type": "sse", + "url": "http://localhost:5271/sse" + } + } +} +``` + +### Generic MCP Client + +Any MCP-compatible client can connect using: +- **Transport**: SSE (Server-Sent Events) +- **Endpoint**: `http://localhost:5271/sse` + +## Working with Asset References + +Many component properties (e.g. `ModelComponent.Model`, `BackgroundComponent.Texture`, `UIComponent.Page`) are asset references that point to other assets in the project. These can be set using `modify_component update` or `set_asset_property`. + +### JSON Format + +Asset reference values accept the following JSON formats: + +```json +// Object with assetId (preferred) +{"Model": {"assetId": "12345678-1234-1234-1234-123456789abc"}} + +// Object with assetRef (matches serialization output, for round-tripping) +{"Model": {"assetRef": "12345678-1234-1234-1234-123456789abc"}} + +// String shorthand +{"Model": "12345678-1234-1234-1234-123456789abc"} + +// Clear the reference +{"Model": null} +``` + +Use `query_assets` to find the asset ID for the asset you want to reference. + +### Example: Setting a Model on an Entity + +1. Find a model asset: `query_assets` with `type: "ModelAsset"` +2. Add a ModelComponent: `modify_component` with `action: "add"`, `componentType: "ModelComponent"` +3. Set the model: `modify_component` with `action: "update"`, `properties: '{"Model":{"assetId":""}}'` +4. Verify: `capture_viewport` to see the model rendered in the scene + +## Working with Polymorphic Properties + +Some component and asset properties are typed as interfaces or abstract classes (e.g. `LightComponent.Type` is `ILight`, `MaterialAttributes.Diffuse` is `IMaterialDiffuseFeature`). These properties accept a concrete type name to create the appropriate instance. + +### JSON Format + +Polymorphic values accept two formats: + +```json +// Format 1: String — creates a default instance of the named type +{"Type": "LightPoint"} + +// Format 2: Object with $type — creates instance with inline property values +{"Type": {"$type": "LightPoint", "Radius": 5.0, "Color": {"r": 1.0, "g": 0.5, "b": 0.0}}} + +// Clear the value +{"Type": null} +``` + +Type names are resolved in order: `[DataContract]` alias, short class name, fully qualified name. Resolution is case-insensitive. + +If an invalid type name is provided, the error message lists all available concrete types for that property. + +### Common Polymorphic Properties + +| Property Path | Interface Type | Available Concrete Types | +|--------------|---------------|-------------------------| +| `LightComponent.Type` | `ILight` | `LightDirectional`, `LightPoint`, `LightSpot`, `LightAmbient`, `LightSkybox` | +| `MaterialAttributes.Diffuse` | `IMaterialDiffuseFeature` | `MaterialDiffuseMapFeature`, `MaterialDiffuseLambertModelFeature`, `MaterialDiffuseCelShadingModelFeature` | +| `MaterialAttributes.Specular` | `IMaterialSpecularFeature` | `MaterialMetalnessMapFeature`, `MaterialSpecularMapFeature` | +| `MaterialAttributes.Transparency` | `IMaterialTransparencyFeature` | `MaterialTransparencyAdditiveFeature`, `MaterialTransparencyBlendFeature`, `MaterialTransparencyCutoffFeature` | +| `MaterialAttributes.Emissive` | `IMaterialEmissiveFeature` | `MaterialEmissiveMapFeature` | + +### Example: Changing a Light Type + +1. Add a LightComponent: `modify_component` with `action: "add"`, `componentType: "LightComponent"` +2. Change to point light: `modify_component` with `action: "update"`, `properties: '{"Type": "LightPoint"}'` +3. Set with properties: `modify_component` with `action: "update"`, `properties: '{"Type": {"$type": "LightPoint", "Radius": 10.0}}'` +4. Verify: `get_entity` to see the updated component + +## Root Assets & Build Inclusion + +Stride uses **root assets** to determine which assets get compiled into the game. Only root assets and their dependencies are included in the build output. If an asset (such as a scene) is not marked as root, it will not be available at runtime. + +### Typical Workflow + +1. **Create or find your asset**: Use `create_asset` or `query_assets` to get the asset ID +2. **Mark it as root**: `manage_root_assets` with `action: "add"` and the asset ID +3. **Build**: `build_project` to compile — root assets and their dependencies are included +4. **Verify**: `manage_root_assets` with `action: "list"` to see all current root assets + +### Important Notes + +- **Scenes** must typically be added as root assets to be playable in the built game +- An asset's dependencies are automatically included when the asset is root — you don't need to mark each dependency individually +- Use `get_editor_status` to see `rootAssetCount` for the current project +- Root asset changes support undo/redo like all other editor operations + +## Project Reload Behavior + +### save_project +Writes the editor's in-memory state to disk. This **overwrites** any external changes made to scene/asset YAML files. If you have modified project files externally, use `restart_game_studio` first to load those changes into the editor. + +### reload_scene +Closes and reopens a single scene editor tab. Useful after `build_project` completes (to pick up new script component types) or when the scene editor appears stale. Unsaved changes to that scene are discarded. + +### restart_game_studio +Triggers a full GameStudio restart (equivalent to File > Reload project). The MCP connection will be lost — the client must wait for the new GameStudio instance to start and reconnect to the new MCP server. If there are unsaved changes, the user will see a Save/Don't Save/Cancel dialog. + +## Integration Tests + +See [`Stride.GameStudio.Mcp.Tests/README.md`](../Stride.GameStudio.Mcp.Tests/README.md) for integration test setup and instructions. diff --git a/sources/editor/Stride.GameStudio.Mcp/Stride.GameStudio.Mcp.csproj b/sources/editor/Stride.GameStudio.Mcp/Stride.GameStudio.Mcp.csproj new file mode 100644 index 0000000000..45fd56e76a --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Stride.GameStudio.Mcp.csproj @@ -0,0 +1,22 @@ + + + + $(StrideEditorTargetFramework) + win-x64 + false + enable + enable + + + + + + + + + + + + + + diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/AddSpriteFrameTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/AddSpriteFrameTool.cs new file mode 100644 index 0000000000..e9a51c8cc8 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/AddSpriteFrameTool.cs @@ -0,0 +1,120 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Sprite; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.IO; +using Stride.Core.Mathematics; +using Stride.Core.Presentation.Services; +using Stride.Core.Quantum; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class AddSpriteFrameTool +{ + [McpServerTool(Name = "add_sprite_frame"), Description("Adds a new sprite frame (SpriteInfo) to a SpriteSheetAsset's Sprites collection. This operation supports undo/redo.")] + public static async Task AddSpriteFrame( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the SpriteSheetAsset")] string assetId, + [Description("Name for the sprite frame")] string name, + [Description("Optional source image file path")] string? sourceFile = null, + [Description("Optional texture region X position")] int? textureRegionX = null, + [Description("Optional texture region Y position")] int? textureRegionY = null, + [Description("Optional texture region width")] int? textureRegionWidth = null, + [Description("Optional texture region height")] int? textureRegionHeight = null, + [Description("Optional pixels per unit (default 100)")] float? pixelsPerUnit = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", frame = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", frame = (object?)null }; + } + + if (assetVm.Asset is not SpriteSheetAsset spriteSheet) + { + return new { error = $"Asset is not a SpriteSheetAsset: {assetVm.Name} ({assetVm.AssetType.Name})", frame = (object?)null }; + } + + // Create new SpriteInfo + var spriteInfo = new SpriteInfo { Name = name }; + + if (!string.IsNullOrEmpty(sourceFile)) + { + spriteInfo.Source = new UFile(sourceFile); + } + + if (textureRegionX.HasValue || textureRegionY.HasValue || textureRegionWidth.HasValue || textureRegionHeight.HasValue) + { + spriteInfo.TextureRegion = new Rectangle( + textureRegionX ?? 0, + textureRegionY ?? 0, + textureRegionWidth ?? 0, + textureRegionHeight ?? 0); + } + + if (pixelsPerUnit.HasValue) + { + spriteInfo.PixelsPerUnit = pixelsPerUnit.Value; + } + + // Generate collection IDs + AssetCollectionItemIdHelper.GenerateMissingItemIds(spriteInfo); + + // Get the property graph node for the Sprites collection + var rootNode = assetVm.PropertyGraph?.RootNode; + if (rootNode == null) + { + return new { error = "Cannot access property graph for this asset.", frame = (object?)null }; + } + + var spritesMember = rootNode.TryGetChild(nameof(SpriteSheetAsset.Sprites)); + if (spritesMember?.Target == null) + { + return new { error = "Cannot access Sprites collection in property graph.", frame = (object?)null }; + } + + var spritesNode = spritesMember.Target; + + // Add within undo/redo transaction + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + spritesNode.Add(spriteInfo); + undoRedoService.SetName(transaction, $"Add sprite frame '{name}'"); + } + + var newIndex = spriteSheet.Sprites.Count - 1; + + return new + { + error = (string?)null, + frame = (object)new + { + index = newIndex, + name, + totalFrames = spriteSheet.Sprites.Count, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/AddUIElementTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/AddUIElementTool.cs new file mode 100644 index 0000000000..042f119780 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/AddUIElementTool.cs @@ -0,0 +1,146 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Presentation.ViewModel; +using Stride.Assets.UI; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.UI; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class AddUIElementTool +{ + [McpServerTool(Name = "add_ui_element"), Description("Creates a new UI element and adds it to a UIPageAsset hierarchy. Supported types: Button, TextBlock, Grid, StackPanel, Canvas, ImageElement, EditText, ToggleButton, Slider, ScrollViewer, ContentDecorator, Border, UniformGrid. This operation supports undo/redo.")] + public static async Task AddUIElement( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the UIPageAsset")] string assetId, + [Description("UI element type name (e.g. 'Button', 'TextBlock', 'StackPanel', 'Grid')")] string elementType, + [Description("Optional name for the element")] string? name = null, + [Description("Optional parent element ID (GUID). Must be a Panel or ContentControl. If omitted, adds to root.")] string? parentId = null, + [Description("Optional insertion index within parent's children. Defaults to append.")] int? index = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", element = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm is not UIBaseViewModel uiPageVm) + { + var errorMsg = assetVm == null + ? $"Asset not found: {assetId}" + : $"Asset is not a UI page: {assetVm.Name} ({assetVm.AssetType.Name})"; + return new { error = errorMsg, element = (object?)null }; + } + + var uiAsset = (UIAssetBase)uiPageVm.Asset; + + // Resolve the UIElement type from the type name + var uiElementAssembly = typeof(UIElement).Assembly; + var resolvedType = uiElementAssembly.GetTypes() + .FirstOrDefault(t => t.Name == elementType + && typeof(UIElement).IsAssignableFrom(t) + && !t.IsAbstract); + + if (resolvedType == null) + { + var availableTypes = uiElementAssembly.GetTypes() + .Where(t => typeof(UIElement).IsAssignableFrom(t) && !t.IsAbstract && t.IsPublic) + .Select(t => t.Name) + .OrderBy(n => n) + .ToArray(); + return new + { + error = $"Unknown UI element type: '{elementType}'. Available types: {string.Join(", ", availableTypes)}", + element = (object?)null, + }; + } + + // Instantiate the element + UIElement newElement; + try + { + newElement = (UIElement)Activator.CreateInstance(resolvedType)!; + } + catch (Exception ex) + { + return new { error = $"Failed to create UI element of type '{elementType}': {ex.Message}", element = (object?)null }; + } + + if (!string.IsNullOrEmpty(name)) + { + newElement.Name = name; + } + + // Generate collection IDs + AssetCollectionItemIdHelper.GenerateMissingItemIds(newElement); + + // Wrap in UIElementDesign collection + var collection = new AssetPartCollection + { + new UIElementDesign(newElement) + }; + + // Resolve parent element if specified + UIElement? parentElement = null; + if (!string.IsNullOrEmpty(parentId)) + { + if (!Guid.TryParse(parentId, out var parentGuid)) + { + return new { error = $"Invalid parent element ID format: {parentId}", element = (object?)null }; + } + + if (!uiAsset.Hierarchy.Parts.TryGetValue(parentGuid, out var parentDesign)) + { + return new { error = $"Parent element not found: {parentId}", element = (object?)null }; + } + parentElement = parentDesign.UIElement; + } + + // Determine insertion index (-1 means append, which InsertUIElement handles) + int insertIndex = index ?? -1; + + // Add to the hierarchy via property graph + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + uiPageVm.InsertUIElement( + collection, + collection.Single().Value, + parentElement, + insertIndex); + + undoRedoService.SetName(transaction, $"Add UI element '{name ?? elementType}'"); + } + + return new + { + error = (string?)null, + element = (object)new + { + id = newElement.Id.ToString(), + name = newElement.Name ?? "", + type = newElement.GetType().Name, + parentId = parentElement?.Id.ToString(), + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/AssetDependenciesTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/AssetDependenciesTool.cs new file mode 100644 index 0000000000..aa69e82626 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/AssetDependenciesTool.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class AssetDependenciesTool +{ + [McpServerTool(Name = "get_asset_dependencies"), Description("Returns the dependency graph for an asset: what assets reference it (inbound/referencedBy) and what assets it references (outbound/references). Also reports broken references. Critical for understanding impact before deleting or modifying an asset.")] + public static async Task GetAssetDependencies( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID (GUID from query_assets)")] string assetId, + [Description("Direction to search: 'in' (who references me), 'out' (what I reference), or 'both' (default)")] string direction = "both", + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", dependencies = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", dependencies = (object?)null }; + } + + var searchOptions = direction.ToLowerInvariant() switch + { + "in" => AssetDependencySearchOptions.In, + "out" => AssetDependencySearchOptions.Out, + _ => AssetDependencySearchOptions.InOut, + }; + + var deps = session.DependencyManager.ComputeDependencies(id, searchOptions, ContentLinkType.Reference); + if (deps == null) + { + return new { error = $"Could not compute dependencies for asset: {assetId}", dependencies = (object?)null }; + } + + var referencedBy = deps.LinksIn.Select(link => + { + var refVm = session.GetAssetById(link.Item.Id); + return new + { + id = link.Item.Id.ToString(), + name = refVm?.Name ?? link.Item.Location.ToString(), + type = refVm?.Asset.GetType().Name ?? "Unknown", + url = link.Item.Location.ToString(), + }; + }).ToList(); + + var references = deps.LinksOut.Select(link => + { + var refVm = session.GetAssetById(link.Item.Id); + return new + { + id = link.Item.Id.ToString(), + name = refVm?.Name ?? link.Item.Location.ToString(), + type = refVm?.Asset.GetType().Name ?? "Unknown", + url = link.Item.Location.ToString(), + }; + }).ToList(); + + var brokenRefs = deps.BrokenLinksOut.Select(link => new + { + id = link.Element.Id.ToString(), + url = link.Element.Location?.ToString() ?? "unknown", + }).ToList(); + + return new + { + error = (string?)null, + dependencies = (object)new + { + assetId = assetVm.Id.ToString(), + assetName = assetVm.Name, + assetType = assetVm.Asset.GetType().Name, + referencedBy, + references, + brokenReferences = brokenRefs, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/AssetDetailsTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/AssetDetailsTool.cs new file mode 100644 index 0000000000..254df19e0a --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/AssetDetailsTool.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class AssetDetailsTool +{ + [McpServerTool(Name = "get_asset_details"), Description("Returns detailed information about a specific asset, including all its serializable properties (via [DataMember] reflection), source file, archetype, tags, directory, and dirty state. Use query_assets to find asset IDs first.")] + public static async Task GetAssetDetails( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID (GUID from query_assets)")] string assetId, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", asset = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", asset = (object?)null }; + } + + var asset = assetVm.Asset; + var properties = JsonTypeConverter.SerializeDataMembers(asset); + + string? sourceFile = null; + if (asset is AssetWithSource aws && aws.Source != null) + { + sourceFile = aws.Source.ToString(); + } + + string? archetypeId = null; + string? archetypeUrl = null; + if (asset.Archetype != null) + { + archetypeId = asset.Archetype.Id.ToString(); + archetypeUrl = asset.Archetype.Location?.ToString(); + } + + return new + { + error = (string?)null, + asset = (object)new + { + id = assetVm.Id.ToString(), + name = assetVm.Name, + type = asset.GetType().Name, + url = assetVm.Url, + directory = assetVm.Directory?.Path ?? "", + package = assetVm.Directory?.Package?.Name ?? "", + isDirty = assetVm.IsDirty, + sourceFile, + archetype = archetypeId != null ? new { id = archetypeId, url = archetypeUrl } : null, + tags = assetVm.Tags.ToArray(), + properties, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/AssetReimportHelper.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/AssetReimportHelper.cs new file mode 100644 index 0000000000..289d9c2783 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/AssetReimportHelper.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Diagnostics; +using Stride.Core.Presentation.Services; + +namespace Stride.GameStudio.Mcp.Tools; + +/// +/// Shared helper for reimporting assets whose source files have changed on disk. +/// Used by both and . +/// Must be called on the UI thread. +/// +internal static class AssetReimportHelper +{ + /// + /// Reimports all assets whose source files have been modified on disk. + /// Returns the list of reimported asset URLs. + /// + public static async Task> ReimportModifiedAssets(SessionViewModel session) + { + var assetsToReimport = session.AllAssets + .Where(a => a.Sources.NeedUpdateFromSource) + .ToList(); + + if (assetsToReimport.Count == 0) + return []; + + var logger = new LoggerResult(); + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + var tasks = assetsToReimport + .Select(a => a.Sources.UpdateAssetFromSource(logger)) + .ToList(); + await Task.WhenAll(tasks); + undoRedoService.SetName(transaction, $"Reimport {tasks.Count} asset(s) from source"); + } + + return assetsToReimport.Select(a => a.Url).ToList(); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/BuildProjectTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/BuildProjectTool.cs new file mode 100644 index 0000000000..27621bc419 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/BuildProjectTool.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Diagnostics; +using Stride.Core.IO; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class BuildProjectTool +{ + private static Task? _buildWrapperTask; + private static LoggerResult? _currentBuildLogger; + private static string? _lastBuildProject; + private static string? _lastAssemblyPath; + private static volatile bool _isCanceled; + private static readonly object _buildLock = new(); + + internal static (Task? wrapperTask, LoggerResult? logger, string? project, string? assemblyPath, bool isCanceled) GetBuildState() + { + lock (_buildLock) + { + return (_buildWrapperTask, _currentBuildLogger, _lastBuildProject, _lastAssemblyPath, _isCanceled); + } + } + + [McpServerTool(Name = "build_project"), Description("Triggers a build of the current game project. Automatically saves all pending changes before building (matching the editor's Build button behavior). The build runs asynchronously; use get_build_status to check progress. After a successful build, call reload_assemblies to make new script types available. Only one build can run at a time.")] + public static async Task BuildProject( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("Build configuration: 'Debug' or 'Release'")] string? configuration = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeTaskOnUIThread(async () => + { + lock (_buildLock) + { + // Check if a build is already in progress + if (_buildWrapperTask != null && !_buildWrapperTask.IsCompleted) + { + return new { error = "A build is already in progress. Use get_build_status to check or wait for it to complete.", build = (object?)null }; + } + } + + // Reimport any assets whose source files have changed on disk + var reimportedAssets = await AssetReimportHelper.ReimportModifiedAssets(session); + + // Auto-save before building (matching the editor's Build button behavior via PrepareBuild) + var saved = await session.SaveSession(); + if (!saved) + { + return new { error = "Save failed. The project must be saved before building — check the editor log for details.", build = (object?)null }; + } + + lock (_buildLock) + { + var currentProject = session.CurrentProject; + if (currentProject == null) + { + return new { error = "No current project is set in the session.", build = (object?)null }; + } + + var projectPath = currentProject.ProjectPath?.ToOSPath(); + if (string.IsNullOrEmpty(projectPath) || !File.Exists(projectPath)) + { + return new { error = $"Project path not found: {projectPath}", build = (object?)null }; + } + + var config = configuration ?? "Debug"; + if (config != "Debug" && config != "Release") + { + return new { error = $"Invalid configuration: '{config}'. Expected 'Debug' or 'Release'.", build = (object?)null }; + } + + var extraProperties = new Dictionary(); + if (!string.IsNullOrEmpty(session.SolutionPath?.ToOSPath())) + { + var solutionPath = UPath.Combine(Environment.CurrentDirectory, session.SolutionPath); + extraProperties["SolutionPath"] = solutionPath.ToOSPath(); + extraProperties["SolutionDir"] = solutionPath.GetParent().ToOSPath() + Path.DirectorySeparatorChar; + } + + var logger = new LoggerResult(); + _currentBuildLogger = logger; + _lastBuildProject = projectPath; + _lastAssemblyPath = null; + _isCanceled = false; + + // Wrap the build in a Task.Run so we never expose Task at the field level. + // VSProjectHelper.CompileProjectAssemblyAsync returns ICancellableAsyncBuild whose + // BuildTask property is Task, requiring Microsoft.Build references. + // By awaiting it inside Task.Run, we only store a plain Task. + var localProjectPath = projectPath; + var localConfig = config; + var localExtraProperties = extraProperties; + var localLogger = logger; + _buildWrapperTask = Task.Run(async () => + { + var asyncBuild = VSProjectHelper.CompileProjectAssemblyAsync( + localProjectPath, localLogger, "Build", localConfig, "AnyCPU", localExtraProperties); + + if (asyncBuild == null) + { + localLogger.Error("Failed to start build. The project may not have a valid TargetPath."); + return; + } + + lock (_buildLock) + { + _lastAssemblyPath = asyncBuild.AssemblyPath; + } + + await asyncBuild.BuildTask; + + if (asyncBuild.IsCanceled) + { + _isCanceled = true; + } + }); + + return new + { + error = (string?)null, + build = (object)new + { + status = "started", + project = Path.GetFileName(projectPath), + configuration = config, + reimportedAssets = reimportedAssets.Count > 0 + ? reimportedAssets + : null, + }, + }; + } + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/CaptureViewportTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/CaptureViewportTool.cs new file mode 100644 index 0000000000..af0060559e --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/CaptureViewportTool.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; +using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class CaptureViewportTool +{ + [McpServerTool(Name = "capture_viewport"), Description("IMPORTANT: This is the primary way to verify your changes and the only way to see the actual rendered result. Captures a PNG screenshot of the viewport for a scene or UI page that is open in the editor. The asset must already be open (use open_scene or open_ui_page first). Returns the image as a base64-encoded PNG. Use this to visually verify entity placement, lighting, UI layout, model references, and other visual aspects after making modifications. TIP: To see a Camera's preview (what the game camera sees), select the Camera entity first using select_entity — selecting it activates the camera preview overlay in the viewport without needing to activate the camera component. Use navigate_viewport to control the editor camera angle before capturing.")] + public static async Task> CaptureViewport( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene or UI page to capture")] string assetId, + CancellationToken cancellationToken = default) + { + // Get the screenshot service on the UI thread (Controller.GetService requires dispatcher) + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return (error: "Invalid asset ID format. Expected a GUID.", service: (IEditorGameScreenshotService?)null); + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return (error: $"Asset not found: {assetId}", service: (IEditorGameScreenshotService?)null); + } + + // Verify the asset type supports viewport capture + if (assetVm is not SceneViewModel && assetVm is not UIPageViewModel) + { + return (error: $"Asset type '{assetVm.AssetType.Name}' does not have a viewport. Only scenes and UI pages support viewport capture.", service: (IEditorGameScreenshotService?)null); + } + + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(assetVm, out var editor)) + { + var openHint = assetVm is SceneViewModel ? "open_scene" : "open_ui_page"; + return (error: $"Asset is not open in the editor. Use {openHint} first: {assetId}", service: (IEditorGameScreenshotService?)null); + } + + if (!editor.SceneInitialized) + { + return (error: "Editor is still initializing. Please wait and try again.", service: (IEditorGameScreenshotService?)null); + } + + var screenshotService = editor.GetEditorGameService(); + if (screenshotService == null) + { + return (error: "Screenshot service is not available for this editor.", service: (IEditorGameScreenshotService?)null); + } + + return (error: (string?)null, service: (IEditorGameScreenshotService?)screenshotService); + }, cancellationToken); + + if (result.error != null) + { + return [new TextContentBlock { Text = result.error }]; + } + + try + { + var pngBytes = await result.service!.CaptureViewportAsync(); + var base64 = Convert.ToBase64String(pngBytes); + + return + [ + new ImageContentBlock + { + Data = base64, + MimeType = "image/png", + }, + ]; + } + catch (Exception ex) + { + return [new TextContentBlock { Text = $"Failed to capture viewport: {ex.Message}" }]; + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/CreateAssetTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/CreateAssetTool.cs new file mode 100644 index 0000000000..568a925b35 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/CreateAssetTool.cs @@ -0,0 +1,309 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Diagnostics; +using Stride.Core.IO; +using Stride.Core.Presentation.Services; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class CreateAssetTool +{ + [McpServerTool(Name = "create_asset"), Description("Creates a new asset, optionally importing from a source file. Without 'source': creates a blank asset of the given type. With 'source': imports the file (FBX, GLTF, OBJ, PNG, JPG, WAV, etc.) using the appropriate importer, which may create multiple assets (e.g. a model + materials + textures). When importing, 'assetType' is optional — the importer auto-detects it. Use query_assets to verify created assets.")] + public static async Task CreateAsset( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset type name (e.g. 'MaterialAsset', 'ModelAsset'). Required when creating blank assets. Optional when importing from source — the importer auto-detects the type.")] string? assetType = null, + [Description("The name for the new asset. When importing, defaults to the source filename.")] string? name = null, + [Description("Directory path within the package (e.g. 'Models/Characters'). Created if it doesn't exist.")] string? directory = null, + [Description("Absolute path to a source file to import (e.g. 'C:/Models/character.fbx', 'C:/Textures/wall.png'). The file must exist on disk. Supported formats: FBX, GLTF, GLB, OBJ, DAE, 3DS, BLEND, PNG, JPG, TGA, DDS, BMP, PSD, TIF, WAV, MP3, OGG, MP4, and more.")] string? source = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + // Import from source file + if (!string.IsNullOrEmpty(source)) + { + return ImportFromSource(session, source, name, directory, assetType); + } + + // Create blank asset + if (string.IsNullOrEmpty(assetType)) + { + return new { error = "Either 'assetType' or 'source' must be provided.", assets = (object?)null, asset = (object?)null }; + } + + if (string.IsNullOrEmpty(name)) + { + return new { error = "'name' is required when creating a blank asset.", assets = (object?)null, asset = (object?)null }; + } + + return CreateBlankAsset(session, assetType, name, directory); + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object ImportFromSource(SessionViewModel session, string sourcePath, string? name, string? directory, string? assetTypeFilter) + { + if (!File.Exists(sourcePath)) + { + return new { error = $"Source file not found: {sourcePath}", assets = (object?)null, asset = (object?)null }; + } + + var importers = AssetRegistry.FindImporterForFile(sourcePath).ToList(); + if (importers.Count == 0) + { + return new { error = $"No importer found for file: {sourcePath}. Supported formats include: FBX, GLTF, GLB, OBJ, DAE, 3DS, PNG, JPG, TGA, DDS, WAV, MP3, OGG.", assets = (object?)null, asset = (object?)null }; + } + + var importer = importers.First(); + + // Set up import parameters + var importParameters = importer.GetDefaultParameters(false); + importParameters.Logger = new LoggerResult(); + + // If a type filter is specified, only enable that type + if (!string.IsNullOrEmpty(assetTypeFilter)) + { + var filterType = ResolveAssetType(assetTypeFilter); + if (filterType != null) + { + foreach (var key in importParameters.SelectedOutputTypes.Keys.ToList()) + { + importParameters.SelectedOutputTypes[key] = key == filterType; + } + } + } + + // Run the importer + var sourceFile = new UFile(sourcePath); + List importedItems; + try + { + importedItems = importer.Import(sourceFile, importParameters).ToList(); + } + catch (Exception ex) + { + return new { error = $"Import failed: {ex.Message}", assets = (object?)null, asset = (object?)null }; + } + + if (importedItems.Count == 0) + { + return new { error = "Import produced no assets. The file may be empty or unsupported.", assets = (object?)null, asset = (object?)null }; + } + + // Find the target package + var package = session.LocalPackages.FirstOrDefault(p => p.IsEditable); + if (package == null) + { + return new { error = "No editable package found in the session.", assets = (object?)null, asset = (object?)null }; + } + + var dirPath = directory ?? ""; + var targetDir = string.IsNullOrEmpty(dirPath) ? "" : dirPath; + + // Add all imported assets to the package + var undoRedoService = session.ServiceProvider.Get(); + var createdAssets = new List(); + + using (var transaction = undoRedoService.CreateTransaction()) + { + var loggerResult = new LoggerResult(); + + foreach (var item in importedItems) + { + // Compute the asset location + var assetName = item.Location.GetFileNameWithoutExtension(); + + // Override name for the first/primary asset if user specified one + if (name != null && item == importedItems[0]) + { + assetName = name; + } + + var assetUrl = string.IsNullOrEmpty(targetDir) ? assetName : $"{targetDir}/{assetName}"; + + // Ensure unique name by appending suffix if collision + var baseUrl = assetUrl; + int suffix = 1; + while (package.Package.Assets.Find(assetUrl) != null) + { + assetUrl = $"{baseUrl}_{suffix++}"; + } + + var newItem = new AssetItem(assetUrl, item.Asset); + AssetCollectionItemIdHelper.GenerateMissingItemIds(newItem.Asset); + + var directoryVm = package.GetOrCreateAssetDirectory(targetDir, canUndoRedoCreation: true); + var assetVm = package.CreateAsset(directoryVm, newItem, true, loggerResult); + + if (assetVm != null) + { + createdAssets.Add(new + { + id = assetVm.Id.ToString(), + name = assetVm.Name, + type = item.Asset.GetType().Name, + url = assetVm.Url, + }); + } + } + + undoRedoService.SetName(transaction, $"Import from '{Path.GetFileName(sourcePath)}'"); + } + + if (createdAssets.Count == 0) + { + return new { error = "Import completed but no assets were added to the project.", assets = (object?)null, asset = (object?)null }; + } + + return new + { + error = (string?)null, + assets = (object)createdAssets, + // Also return 'asset' pointing to the first one for backwards compatibility + asset = (object?)createdAssets[0], + }; + } + + private static object CreateBlankAsset(SessionViewModel session, string assetType, string name, string? directory) + { + // Resolve asset type + var resolvedType = ResolveAssetType(assetType); + if (resolvedType == null) + { + var availableTypes = AssetRegistry.GetPublicTypes() + .Select(t => t.Name) + .OrderBy(n => n) + .ToArray(); + return new + { + error = $"Asset type not found: '{assetType}'. Available types: {string.Join(", ", availableTypes.Take(20))}", + assets = (object?)null, + asset = (object?)null, + }; + } + + // Find a factory for this type + Asset? newAsset = null; + foreach (var factory in AssetRegistry.GetAllAssetFactories()) + { + if (factory.AssetType == resolvedType) + { + newAsset = factory.New(); + break; + } + } + + // Fallback to Activator if no factory found + if (newAsset == null) + { + try + { + newAsset = (Asset)Activator.CreateInstance(resolvedType)!; + } + catch (Exception ex) + { + return new + { + error = $"Cannot create instance of '{resolvedType.Name}': {ex.Message}", + assets = (object?)null, + asset = (object?)null, + }; + } + } + + // Find the target package + var package = session.LocalPackages.FirstOrDefault(p => p.IsEditable); + if (package == null) + { + return new { error = "No editable package found in the session.", assets = (object?)null, asset = (object?)null }; + } + + // Set up asset item + AssetCollectionItemIdHelper.GenerateMissingItemIds(newAsset); + var assetUrl = string.IsNullOrEmpty(directory) ? name : $"{directory}/{name}"; + var assetItem = new AssetItem(assetUrl, newAsset); + + // Create directory if needed + var dirPath = directory ?? ""; + var directoryVm = package.GetOrCreateAssetDirectory(dirPath, canUndoRedoCreation: true); + + // Create with undo/redo + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + var loggerResult = new LoggerResult(); + var assetVm = package.CreateAsset(directoryVm, assetItem, true, loggerResult); + + if (assetVm == null) + { + return new + { + error = $"Failed to create asset. Log: {string.Join("; ", loggerResult.Messages.Select(m => m.Text))}", + assets = (object?)null, + asset = (object?)null, + }; + } + + undoRedoService.SetName(transaction, $"Create {resolvedType.Name} '{name}'"); + + var assetInfo = new + { + id = assetVm.Id.ToString(), + name = assetVm.Name, + type = resolvedType.Name, + url = assetVm.Url, + }; + + return new + { + error = (string?)null, + assets = (object?)null, + asset = (object?)assetInfo, + }; + } + } + + private static Type? ResolveAssetType(string typeName) + { + var publicTypes = AssetRegistry.GetPublicTypes(); + + // Try exact name match + var match = publicTypes.FirstOrDefault(t => t.Name == typeName); + if (match != null) + return match; + + // Try full name match + match = publicTypes.FirstOrDefault(t => t.FullName == typeName); + if (match != null) + return match; + + // Try case-insensitive match + match = publicTypes.FirstOrDefault(t => + string.Equals(t.Name, typeName, StringComparison.OrdinalIgnoreCase)); + if (match != null) + return match; + + // Try without "Asset" suffix + if (!typeName.EndsWith("Asset", StringComparison.OrdinalIgnoreCase)) + { + return ResolveAssetType(typeName + "Asset"); + } + + return null; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/CreateEntityTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/CreateEntityTool.cs new file mode 100644 index 0000000000..70881ac9c3 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/CreateEntityTool.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Entities; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class CreateEntityTool +{ + [McpServerTool(Name = "create_entity"), Description("Creates a new entity in a scene. The scene must be open in the editor (use open_scene first). The new entity gets a TransformComponent by default. Returns the new entity's ID. This operation supports undo/redo in the editor.")] + public static async Task CreateEntity( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene to add the entity to")] string sceneId, + [Description("Name for the new entity")] string name, + [Description("Optional parent entity ID. If omitted, entity is added at the root level.")] string? parentId = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", entity = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", entity = (object?)null }; + } + + // Get the editor for this scene + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + { + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", entity = (object?)null }; + } + + // Resolve the parent entity (if specified) + Entity? parentEntity = null; + if (!string.IsNullOrEmpty(parentId)) + { + if (!Guid.TryParse(parentId, out var parentGuid)) + { + return new { error = $"Invalid parent entity ID format: {parentId}", entity = (object?)null }; + } + + var parentAbsId = new AbsoluteId(assetId, parentGuid); + var parentVm = editor.FindPartViewModel(parentAbsId); + if (parentVm is not EntityViewModel parentEntityVm) + { + return new { error = $"Parent entity not found: {parentId}", entity = (object?)null }; + } + parentEntity = parentEntityVm.AssetSideEntity; + } + + // Create the new entity + var newEntity = new Entity { Name = name ?? "Entity" }; + + // Generate collection item IDs required by the asset system + AssetCollectionItemIdHelper.GenerateMissingItemIds(newEntity); + + // Wrap in EntityDesign and add to the scene via property graph + var collection = new AssetPartCollection + { + new EntityDesign(newEntity, "") + }; + + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + int insertIndex = parentEntity?.Transform.Children.Count + ?? sceneVm.Asset.Hierarchy.RootParts.Count; + + sceneVm.AssetHierarchyPropertyGraph.AddPartToAsset( + collection, + collection.Single().Value, + parentEntity, + insertIndex); + + undoRedoService.SetName(transaction, $"Create entity '{name}'"); + } + + return new + { + error = (string?)null, + entity = (object)new + { + id = newEntity.Id.ToString(), + name = newEntity.Name, + parentId = parentEntity?.Id.ToString(), + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/DeleteEntityTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/DeleteEntityTool.cs new file mode 100644 index 0000000000..e185e1d807 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/DeleteEntityTool.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Entities; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class DeleteEntityTool +{ + [McpServerTool(Name = "delete_entity"), Description("Deletes an entity from a scene. The scene must be open in the editor (use open_scene first). This also deletes all child entities. This operation supports undo/redo in the editor.")] + public static async Task DeleteEntity( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene containing the entity")] string sceneId, + [Description("The entity ID to delete")] string entityId, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", deleted = (object?)null }; + } + + if (!Guid.TryParse(entityId, out var entityGuid)) + { + return new { error = "Invalid entity ID format. Expected a GUID.", deleted = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", deleted = (object?)null }; + } + + // Get the editor for this scene + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + { + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", deleted = (object?)null }; + } + + // Find the entity to delete + var absoluteId = new AbsoluteId(assetId, entityGuid); + var partVm = editor.FindPartViewModel(absoluteId); + if (partVm is not EntityViewModel entityVm) + { + return new { error = $"Entity not found in scene: {entityId}", deleted = (object?)null }; + } + + var entityName = entityVm.Name; + var entityDesign = ((IEditorDesignPartViewModel)entityVm).PartDesign; + + // Clear selection to avoid stale references + editor.ClearSelection(); + + // Delete via the property graph with undo/redo support + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + sceneVm.AssetHierarchyPropertyGraph.DeleteParts( + new[] { entityDesign }, + out var mapping); + + var operation = new DeletedPartsTrackingOperation(sceneVm, mapping); + undoRedoService.PushOperation(operation); + + undoRedoService.SetName(transaction, $"Delete entity '{entityName}'"); + } + + return new + { + error = (string?)null, + deleted = (object)new + { + id = entityId, + name = entityName, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/DescribeViewportTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/DescribeViewportTool.cs new file mode 100644 index 0000000000..f05eb48e3a --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/DescribeViewportTool.cs @@ -0,0 +1,211 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; +using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; +using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Mathematics; +using Stride.Engine; +using Stride.Graphics; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class DescribeViewportTool +{ + private const float DefaultAspectRatio = 16f / 9f; + + [McpServerTool(Name = "describe_viewport"), Description("Returns a list of entities visible in the current editor viewport with their projected screen coordinates (normalized 0-1). This helps understand what is rendered in the viewport even when materials or lighting make the image hard to interpret. Use this alongside capture_viewport to correlate visual output with entity positions. The scene must be open in the editor (use open_scene first).")] + public static async Task DescribeViewport( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene whose viewport to describe")] string sceneId, + [Description("Maximum number of entities to return (default 100)")] int maxEntities = 100, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + return new { error = "Invalid scene ID format. Expected a GUID.", result = (object?)null }; + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + return new { error = $"Scene not found: {sceneId}", result = (object?)null }; + + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", result = (object?)null }; + + if (!editor.SceneInitialized) + return new { error = "Editor is still initializing. Please wait and try again.", result = (object?)null }; + + var cameraVmService = editor.GetEditorGameService(); + if (cameraVmService == null) + return new { error = "Camera service is not available for this editor.", result = (object?)null }; + + // Get camera state via SaveSettings snapshot + var snapshot = new Stride.Assets.Presentation.SceneEditor.SceneSettingsData(); + cameraVmService.SaveSettings(snapshot); + + var camPosition = snapshot.CamPosition; + var pitch = snapshot.CamPitchYaw.X; + var yaw = snapshot.CamPitchYaw.Y; + + // Get projection settings from the EditorCameraViewModel + var entityCameraService = editor.GetEditorGameService(); + bool isOrthographic = false; + float fov = 45f; + float nearPlane = 0.1f; + float farPlane = 1000f; + float orthographicSize = 10f; + float aspectRatio = DefaultAspectRatio; + + if (entityCameraService != null) + { + var cam = entityCameraService.Camera; + isOrthographic = cam.OrthographicProjection; + fov = cam.FieldOfView; + nearPlane = cam.NearPlane; + farPlane = cam.FarPlane; + orthographicSize = cam.OrthographicSize; + } + + // Try to get aspect ratio from the camera service (same object implements both interfaces) + if (cameraVmService is Stride.Assets.Presentation.AssetEditors.GameEditor.Game.IEditorGameCameraService gameCameraService) + { + aspectRatio = gameCameraService.AspectRatio; + } + + // Reconstruct view matrix (same formula as EditorGameCameraService.UpdateViewMatrix) + var rotation = Quaternion.Invert(Quaternion.RotationYawPitchRoll(yaw, pitch, 0)); + var viewMatrix = Matrix.Translation(-camPosition) * Matrix.RotationQuaternion(rotation); + + // Reconstruct projection matrix + Matrix projectionMatrix; + if (isOrthographic) + { + var orthoWidth = orthographicSize * aspectRatio; + var orthoHeight = orthographicSize; + projectionMatrix = Matrix.OrthoRH(orthoWidth, orthoHeight, nearPlane, farPlane); + } + else + { + projectionMatrix = Matrix.PerspectiveFovRH(MathUtil.DegreesToRadians(fov), aspectRatio, nearPlane, farPlane); + } + + // Use a 1x1 viewport for normalized coordinates (0-1) + var viewport = new Viewport(0, 0, 1, 1); + + // Walk all entities in the scene asset hierarchy and project them + var sceneAsset = sceneVm.Asset; + var rootEntities = sceneAsset.Hierarchy.RootParts; + var visibleEntities = new List(); + + foreach (var rootEntity in rootEntities) + { + CollectVisibleEntities(rootEntity, Matrix.Identity, viewport, viewMatrix, projectionMatrix, camPosition, farPlane, visibleEntities); + } + + // Sort by distance to camera (nearest first), then cap + var sortedEntities = visibleEntities.Take(maxEntities).ToList(); + + return new + { + error = (string?)null, + result = (object)new + { + sceneId = sceneVm.Id.ToString(), + sceneName = sceneVm.Name, + camera = new + { + position = new { x = camPosition.X, y = camPosition.Y, z = camPosition.Z }, + yawDegrees = MathUtil.RadiansToDegrees(yaw), + pitchDegrees = MathUtil.RadiansToDegrees(pitch), + projection = isOrthographic ? "orthographic" : "perspective", + fieldOfViewDegrees = fov, + nearPlane, + farPlane, + }, + totalEntitiesInScene = sceneAsset.Hierarchy.Parts.Count, + visibleEntityCount = sortedEntities.Count, + entities = sortedEntities, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static void CollectVisibleEntities( + Entity entity, + Matrix parentWorldMatrix, + Viewport viewport, + Matrix viewMatrix, + Matrix projectionMatrix, + Vector3 camPosition, + float farPlane, + List results) + { + // Build this entity's local matrix from its Transform component + var transform = entity.Transform; + var localMatrix = Matrix.Scaling(transform.Scale) + * Matrix.RotationQuaternion(transform.Rotation) + * Matrix.Translation(transform.Position); + var worldMatrix = localMatrix * parentWorldMatrix; + + // Extract world position + var worldPos = worldMatrix.TranslationVector; + + // Compute distance to camera + var distanceToCamera = Vector3.Distance(worldPos, camPosition); + + // Project to screen space + var screenPos = viewport.Project(worldPos, projectionMatrix, viewMatrix, Matrix.Identity); + + // Check if entity is in front of camera and within screen bounds + bool isInFrustum = screenPos.Z >= 0 && screenPos.Z <= 1 + && screenPos.X >= -0.5f && screenPos.X <= 1.5f + && screenPos.Y >= -0.5f && screenPos.Y <= 1.5f; + + var componentNames = entity.Components + .Select(c => c.GetType().Name) + .ToList(); + + results.Add(new + { + id = entity.Id.ToString(), + name = entity.Name, + worldPosition = new { x = Math.Round(worldPos.X, 2), y = Math.Round(worldPos.Y, 2), z = Math.Round(worldPos.Z, 2) }, + screenPosition = isInFrustum + ? new { x = Math.Round(screenPos.X, 3), y = Math.Round(screenPos.Y, 3) } + : null, + distanceToCamera = Math.Round(distanceToCamera, 2), + isVisible = isInFrustum, + components = componentNames, + }); + + // Recurse into children + if (transform != null) + { + foreach (var childTransform in transform.Children) + { + if (childTransform.Entity != null) + { + CollectVisibleEntities(childTransform.Entity, worldMatrix, viewport, viewMatrix, projectionMatrix, camPosition, farPlane, results); + } + } + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/FocusEntityTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/FocusEntityTool.cs new file mode 100644 index 0000000000..e8c555579e --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/FocusEntityTool.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class FocusEntityTool +{ + [McpServerTool(Name = "focus_entity"), Description("Focuses the viewport camera on a specific entity, centering it in the scene view. The scene must already be open in the editor (use open_scene first). This also selects the entity. Use this to navigate the 3D viewport to inspect specific objects.")] + public static async Task FocusEntity( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene containing the entity")] string sceneId, + [Description("The entity ID (GUID from get_scene_tree) to focus on")] string entityId, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", focused = (object?)null }; + } + + if (!Guid.TryParse(entityId, out var entityGuid)) + { + return new { error = "Invalid entity ID format. Expected a GUID.", focused = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", focused = (object?)null }; + } + + // Get the editor for this scene + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + { + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", focused = (object?)null }; + } + + // Find the entity view model + var absoluteId = new AbsoluteId(assetId, entityGuid); + var partVm = editor.FindPartViewModel(absoluteId); + if (partVm is not EntityViewModel entityVm) + { + return new { error = $"Entity not found in scene: {entityId}", focused = (object?)null }; + } + + // Check if the entity is loaded in the game engine (required for camera focus) + if (!entityVm.IsLoaded) + { + return new { error = $"Entity is not loaded in the viewport. It may be in an unloaded sub-scene: {entityId}", focused = (object?)null }; + } + + // Select the entity first + editor.ClearSelection(); + editor.SelectedContent.Add(entityVm); + + // Use the entity's built-in FocusOnEntityCommand to center the camera + // This invokes IEditorGameEntityCameraViewModelService.CenterOnEntity internally + if (entityVm.FocusOnEntityCommand.IsEnabled) + { + entityVm.FocusOnEntityCommand.Execute(); + } + else + { + return new + { + error = (string?)null, + focused = (object)new + { + id = entityVm.AssetSideEntity.Id.ToString(), + name = entityVm.Name, + warning = "Focus command is not available. Entity was selected but camera was not moved.", + }, + }; + } + + return new + { + error = (string?)null, + focused = (object)new + { + id = entityVm.AssetSideEntity.Id.ToString(), + name = entityVm.Name, + warning = (string?)null, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/GetBuildStatusTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/GetBuildStatusTool.cs new file mode 100644 index 0000000000..941aa69597 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/GetBuildStatusTool.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Diagnostics; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class GetBuildStatusTool +{ + [McpServerTool(Name = "get_build_status"), Description("Returns the current build status. Use after build_project to check progress. Returns: 'idle' (no build), 'building' (in progress), 'succeeded', or 'failed' with error messages. Also includes 'assemblyReloadPending' — if true after a successful build, call reload_assemblies to make new script types available.")] + public static Task GetBuildStatus( + SessionViewModel session, + DispatcherBridge dispatcher, + CancellationToken cancellationToken = default) + { + var (wrapperTask, logger, lastProject, assemblyPath, isCanceled) = BuildProjectTool.GetBuildState(); + var projectFileName = lastProject != null ? Path.GetFileName(lastProject) : null; + var reloadPending = ReloadAssembliesTool.IsReloadPending(); + + if (wrapperTask == null) + { + return Task.FromResult(JsonSerializer.Serialize(new + { + status = "idle", + project = (string?)null, + errors = (string[]?)null, + warnings = (string[]?)null, + assemblyPath = (string?)null, + assemblyReloadPending = reloadPending, + }, new JsonSerializerOptions { WriteIndented = true })); + } + + if (!wrapperTask.IsCompleted) + { + return Task.FromResult(JsonSerializer.Serialize(new + { + status = "building", + project = projectFileName, + errors = (string[]?)null, + warnings = (string[]?)null, + assemblyPath = (string?)null, + assemblyReloadPending = reloadPending, + }, new JsonSerializerOptions { WriteIndented = true })); + } + + if (isCanceled) + { + return Task.FromResult(JsonSerializer.Serialize(new + { + status = "canceled", + project = projectFileName, + errors = (string[]?)null, + warnings = (string[]?)null, + assemblyPath = (string?)null, + assemblyReloadPending = reloadPending, + }, new JsonSerializerOptions { WriteIndented = true })); + } + + var hasErrors = logger?.HasErrors ?? false; + var errorMessages = logger?.Messages + .Where(m => m.Type == LogMessageType.Error || m.Type == LogMessageType.Fatal) + .Select(m => m.Text) + .ToArray(); + var warningMessages = logger?.Messages + .Where(m => m.Type == LogMessageType.Warning) + .Select(m => m.Text) + .ToArray(); + + return Task.FromResult(JsonSerializer.Serialize(new + { + status = hasErrors ? "failed" : "succeeded", + project = projectFileName, + errors = errorMessages?.Length > 0 ? errorMessages : null, + warnings = warningMessages?.Length > 0 ? warningMessages : null, + assemblyPath = !hasErrors ? assemblyPath : null, + assemblyReloadPending = reloadPending, + }, new JsonSerializerOptions { WriteIndented = true })); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/GetEditorStatusTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/GetEditorStatusTool.cs new file mode 100644 index 0000000000..ef5be1a734 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/GetEditorStatusTool.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class GetEditorStatusTool +{ + [McpServerTool(Name = "get_editor_status"), Description("Returns the current status of Stride Game Studio, including the loaded project name, solution path, and list of available scenes. Use this tool first to verify the editor is connected and to discover available content.")] + public static async Task GetEditorStatus( + SessionViewModel session, + DispatcherBridge dispatcher, + CancellationToken cancellationToken) + { + var status = await dispatcher.InvokeOnUIThread(() => + { + var currentProject = session.CurrentProject?.Name ?? "(no project)"; + var solutionPath = session.SolutionPath?.ToString() ?? "(none)"; + + var packages = session.LocalPackages + .Select(p => p.Name) + .ToList(); + + var projects = session.LocalPackages + .OfType() + .Select(p => new + { + name = p.Name, + type = p.Type.ToString(), + platform = p.Platform.ToString(), + isCurrentProject = p.IsCurrentProject, + isExecutable = p.Type == ProjectType.Executable, + recommended = p.Type == ProjectType.Executable + && p.Platform == PlatformType.Windows, + }) + .ToList(); + + var allAssets = session.AllAssets.ToList(); + var scenes = allAssets + .Where(a => a.AssetType.Name == "SceneAsset") + .Select(a => new + { + id = a.Id.ToString(), + name = a.Name, + url = a.Url, + }) + .ToList(); + + var assetCount = allAssets.Count; + var rootAssetCount = session.CurrentProject?.RootAssets.Count ?? 0; + + var suppressedDialogs = new List(); + while (DispatcherBridge.RecentSuppressedDialogs.TryDequeue(out var msg)) + suppressedDialogs.Add(msg); + + return new + { + status = "connected", + currentProject, + solutionPath, + packages, + projects, + assetCount, + rootAssetCount, + scenes, + lastSuppressedDialogs = suppressedDialogs.Count > 0 ? suppressedDialogs : null, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(status, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/GetEntityTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/GetEntityTool.cs new file mode 100644 index 0000000000..0ddb144a2d --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/GetEntityTool.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class GetEntityTool +{ + [McpServerTool(Name = "get_entity"), Description("Returns detailed information about a specific entity, including all its components and their properties. The entityId is the entity GUID (from get_scene_tree), and sceneId is needed to locate the entity within the scene hierarchy.")] + public static async Task GetEntity( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene containing the entity")] string sceneId, + [Description("The entity ID (GUID from get_scene_tree)")] string entityId, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var sceneAssetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", entity = (object?)null }; + } + + if (!Guid.TryParse(entityId, out var entityGuid)) + { + return new { error = "Invalid entity ID format. Expected a GUID.", entity = (object?)null }; + } + + var assetVm = session.GetAssetById(sceneAssetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", entity = (object?)null }; + } + + var sceneAsset = sceneVm.Asset; + if (!sceneAsset.Hierarchy.Parts.TryGetValue(entityGuid, out var entityDesign)) + { + return new { error = $"Entity not found in scene: {entityId}", entity = (object?)null }; + } + + var entity = entityDesign.Entity; + var parentId = (string?)null; + if (entity.Transform?.Parent != null) + { + parentId = entity.Transform.Parent.Entity?.Id.ToString(); + } + + var childIds = entity.Transform?.Children + .Where(c => c.Entity != null) + .Select(c => c.Entity.Id.ToString()) + .ToList() ?? []; + + var components = entity.Components + .Select(c => SerializeComponent(c)) + .ToList(); + + return new + { + error = (string?)null, + entity = (object)new + { + id = entity.Id.ToString(), + name = entity.Name, + parentId, + childIds, + components, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object SerializeComponent(EntityComponent component) + { + var properties = JsonTypeConverter.SerializeDataMembers(component); + return new + { + type = component.GetType().Name, + properties, + }; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/GetSceneTreeTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/GetSceneTreeTool.cs new file mode 100644 index 0000000000..c76783f703 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/GetSceneTreeTool.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Entities; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class GetSceneTreeTool +{ + [McpServerTool(Name = "get_scene_tree"), Description("Returns the full entity hierarchy tree for a given scene. Each entity includes its ID, name, and children. Use this to understand the structure of a scene before inspecting individual entities with get_entity.")] + public static async Task GetSceneTree( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene (from get_editor_status or query_assets)")] string sceneId, + [Description("Maximum depth to traverse (default 50)")] int maxDepth = 50, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", scene = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found or asset is not a scene: {sceneId}", scene = (object?)null }; + } + + var sceneAsset = sceneVm.Asset; + var rootEntities = sceneAsset.Hierarchy.RootParts; + + var entityTree = rootEntities + .Select(e => BuildEntityNode(e, 0, maxDepth)) + .ToList(); + + var totalCount = sceneAsset.Hierarchy.Parts.Count; + + return new + { + error = (string?)null, + scene = (object)new + { + id = sceneId, + name = sceneVm.Name, + entityCount = totalCount, + entities = entityTree, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object BuildEntityNode(Entity entity, int depth, int maxDepth) + { + var children = new List(); + + if (depth < maxDepth && entity.Transform != null) + { + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + { + children.Add(BuildEntityNode(childTransform.Entity, depth + 1, maxDepth)); + } + } + } + + var components = entity.Components + .Select(c => c.GetType().Name) + .ToList(); + + return new + { + id = entity.Id.ToString(), + name = entity.Name, + components, + children, + }; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/GetUIElementTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/GetUIElementTool.cs new file mode 100644 index 0000000000..2d82d179c1 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/GetUIElementTool.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.UI; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class GetUIElementTool +{ + [McpServerTool(Name = "get_ui_element"), Description("Returns detailed properties for a specific UI element within a UIPageAsset. Includes parent/child relationships and all serialized properties.")] + public static async Task GetUIElement( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the UIPageAsset")] string assetId, + [Description("The UI element ID (GUID from get_ui_tree)")] string elementId, + CancellationToken cancellationToken = default) + { + try + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", element = (object?)null }; + } + + if (!Guid.TryParse(elementId, out var elementGuid)) + { + return new { error = "Invalid element ID format. Expected a GUID.", element = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", element = (object?)null }; + } + + if (assetVm.Asset is not UIAssetBase uiAsset) + { + return new { error = $"Asset is not a UI page: {assetVm.Name} ({assetVm.AssetType.Name})", element = (object?)null }; + } + + if (!uiAsset.Hierarchy.Parts.TryGetValue(elementGuid, out var design)) + { + return new { error = $"UI element not found: {elementId}", element = (object?)null }; + } + + var element = design.UIElement; + var parentId = element.VisualParent?.Id.ToString(); + var childIds = element.VisualChildren + .Select(c => c.Id.ToString()) + .ToList(); + + var properties = JsonTypeConverter.SerializeDataMembers(element); + + return new + { + error = (string?)null, + element = (object)new + { + id = element.Id.ToString(), + name = element.Name ?? "", + type = element.GetType().Name, + parentId, + childIds, + properties, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { error = $"Internal error: {ex.GetType().Name}: {ex.Message}", element = (object?)null }, new JsonSerializerOptions { WriteIndented = true }); + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/GetUITreeTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/GetUITreeTool.cs new file mode 100644 index 0000000000..1e2fb159fc --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/GetUITreeTool.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.UI; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.UI; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class GetUITreeTool +{ + [McpServerTool(Name = "get_ui_tree"), Description("Returns the full UI element hierarchy tree for a UIPageAsset. Each element includes its ID, name, type, and children. Use this to understand the structure of a UI page before inspecting individual elements with get_ui_element.")] + public static async Task GetUITree( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the UIPageAsset (from query_assets)")] string assetId, + [Description("Maximum depth to traverse (default 50)")] int maxDepth = 50, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", uiPage = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", uiPage = (object?)null }; + } + + if (assetVm.Asset is not UIAssetBase uiAsset) + { + return new { error = $"Asset is not a UI page: {assetVm.Name} ({assetVm.AssetType.Name})", uiPage = (object?)null }; + } + + var rootElements = uiAsset.Hierarchy.RootParts; + + var elementTree = rootElements + .Select(e => BuildElementNode(e, 0, maxDepth)) + .ToList(); + + var totalCount = uiAsset.Hierarchy.Parts.Count; + + return new + { + error = (string?)null, + uiPage = (object)new + { + id = assetId, + name = assetVm.Name, + elementCount = totalCount, + elements = elementTree, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object BuildElementNode(UIElement element, int depth, int maxDepth) + { + var children = new List(); + + if (depth < maxDepth) + { + foreach (var child in element.VisualChildren) + { + children.Add(BuildElementNode(child, depth + 1, maxDepth)); + } + } + + return new + { + id = element.Id.ToString(), + name = element.Name ?? "", + type = element.GetType().Name, + children, + }; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/JsonTypeConverter.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/JsonTypeConverter.cs new file mode 100644 index 0000000000..047a664c92 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/JsonTypeConverter.cs @@ -0,0 +1,707 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using Stride.Core; +using Stride.Core.Annotations; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Extensions; +using Stride.Core.IO; +using Stride.Core.Mathematics; +using Stride.Core.Reflection; +using Stride.Core.Serialization; +using Stride.Core.Serialization.Contents; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +/// +/// Shared utility for serializing/deserializing values between MCP JSON and Stride types. +/// +internal static class JsonTypeConverter +{ + /// + /// Serializes a value to a JSON-compatible representation. + /// Handles Stride math types, collections, entity references, asset references, and DataMember objects. + /// + public static object? SerializeValue(object? value, int depth = 0) + { + if (value == null) + return null; + + if (depth > 3) + return value.ToString(); + + var type = value.GetType(); + + // Primitives and strings + if (value is float f && (float.IsNaN(f) || float.IsInfinity(f))) + return f.ToString(); + if (value is double d && (double.IsNaN(d) || double.IsInfinity(d))) + return d.ToString(); + if (type.IsPrimitive || value is string || value is decimal) + return value; + + // Enums + if (type.IsEnum) + return value.ToString(); + + // Stride math types — float vectors + if (value is Vector2 v2) + return new { x = v2.X, y = v2.Y }; + if (value is Vector3 v3) + return new { x = v3.X, y = v3.Y, z = v3.Z }; + if (value is Vector4 v4) + return new { x = v4.X, y = v4.Y, z = v4.Z, w = v4.W }; + if (value is Quaternion q) + return new { x = q.X, y = q.Y, z = q.Z, w = q.W }; + + // Integer vectors + if (value is Int2 i2) + return new { x = i2.X, y = i2.Y }; + if (value is Int3 i3) + return new { x = i3.X, y = i3.Y, z = i3.Z }; + if (value is Int4 i4) + return new { x = i4.X, y = i4.Y, z = i4.Z, w = i4.W }; + + // Colors + if (value is Color c) + return new { r = (int)c.R, g = (int)c.G, b = (int)c.B, a = (int)c.A }; + if (value is Color3 c3) + return new { r = c3.R, g = c3.G, b = c3.B }; + if (value is Color4 c4) + return new { r = c4.R, g = c4.G, b = c4.B, a = c4.A }; + + // Rectangles and sizes + if (value is RectangleF rf) + return new { x = rf.X, y = rf.Y, width = rf.Width, height = rf.Height }; + if (value is Rectangle ri) + return new { x = ri.X, y = ri.Y, width = ri.Width, height = ri.Height }; + if (value is Size2 s2) + return new { width = s2.Width, height = s2.Height }; + if (value is Size2F s2f) + return new { width = s2f.Width, height = s2f.Height }; + if (value is Size3 s3) + return new { width = s3.Width, height = s3.Height, depth = s3.Depth }; + + // Angle + if (value is AngleSingle angle) + return new { degrees = angle.Degrees }; + + // Matrix — too large for structured serialization + if (value is Matrix) + return value.ToString(); + + // File/directory paths + if (value is UPath path) + return path.ToString(); + + // Asset references — explicit IReference types (AssetReference, UrlReferenceBase) + if (value is IReference assetRef) + return new { assetRef = assetRef.Id.ToString(), url = assetRef.Location?.ToString() }; + + // Asset references — proxy objects with AttachedReference (Model, Material, Texture, etc.) + var attachedRef = AttachedReferenceManager.GetAttachedReference(value); + if (attachedRef != null) + return new { assetRef = attachedRef.Id.ToString(), url = attachedRef.Url }; + + // Entity references + if (value is Entity entity) + return new { entityRef = entity.Id.ToString(), name = entity.Name }; + if (value is EntityComponent comp) + return new { componentRef = comp.GetType().Name, entityId = comp.Entity?.Id.ToString() }; + + // Collections (limited) + if (value is IEnumerable enumerable && value is not string) + { + var items = new List(); + var count = 0; + foreach (var item in enumerable) + { + if (count++ >= 20) // Limit collection output + { + items.Add($"... ({count}+ items total)"); + break; + } + items.Add(SerializeValue(item, depth + 1)); + } + return items; + } + + // Objects with [DataMember] attributes — serialize their members + if (depth < 3) + { + var members = SerializeDataMembers(value, depth + 1); + if (members.Count > 0) + return new { type = type.Name, properties = members }; + } + + // Fallback: just return string representation + return value.ToString(); + } + + /// + /// Serializes all [DataMember] fields and properties of an object to a dictionary. + /// + public static Dictionary SerializeDataMembers(object obj, int depth = 0) + { + var type = obj.GetType(); + var properties = new Dictionary(); + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (field.GetCustomAttribute() == null) + continue; + + try + { + var value = field.GetValue(obj); + properties[field.Name] = SerializeValue(value, depth); + } + catch + { + properties[field.Name] = ""; + } + } + + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.GetCustomAttribute() == null) + continue; + if (!prop.CanRead || prop.GetIndexParameters().Length > 0) + continue; + + try + { + var value = prop.GetValue(obj); + properties[prop.Name] = SerializeValue(value, depth); + } + catch + { + properties[prop.Name] = ""; + } + } + + return properties; + } + + /// + /// Converts a JSON element to the specified target type. + /// Supports primitives, enums, Stride math types, and asset references (when session is provided). + /// + public static object? ConvertJsonToType(JsonElement json, Type targetType, SessionViewModel? session) + { + var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + // Asset reference types — requires session to resolve asset IDs + if (session != null && json.ValueKind != JsonValueKind.Null && + (AssetRegistry.CanBeAssignedToContentTypes(underlyingType, checkIsUrlType: true) + || typeof(AssetReference).IsAssignableFrom(underlyingType))) + { + return ConvertAssetReference(json, underlyingType, session); + } + + // Null clears asset reference properties even without session + if (json.ValueKind == JsonValueKind.Null && + (AssetRegistry.CanBeAssignedToContentTypes(underlyingType, checkIsUrlType: true) + || typeof(AssetReference).IsAssignableFrom(underlyingType))) + { + return null; + } + + // Polymorphic types (interfaces/abstract classes) — resolve concrete type and instantiate + if (IsPolymorphicType(underlyingType)) + return ConvertPolymorphicValue(json, underlyingType, session); + + return ConvertJsonToType(json, targetType); + } + + /// + /// Converts a JSON element to the specified target type. + /// Supports primitives, enums, and Stride math types. + /// + public static object? ConvertJsonToType(JsonElement json, Type targetType) + { + // Handle nullable types + var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + if (json.ValueKind == JsonValueKind.Null) + return null; + + // Primitive types + if (underlyingType == typeof(bool)) + return json.GetBoolean(); + if (underlyingType == typeof(int)) + return json.GetInt32(); + if (underlyingType == typeof(float)) + return json.GetSingle(); + if (underlyingType == typeof(double)) + return json.GetDouble(); + if (underlyingType == typeof(string)) + return json.GetString(); + if (underlyingType == typeof(long)) + return json.GetInt64(); + if (underlyingType == typeof(byte)) + return json.GetByte(); + if (underlyingType == typeof(short)) + return json.GetInt16(); + if (underlyingType == typeof(ushort)) + return json.GetUInt16(); + if (underlyingType == typeof(uint)) + return json.GetUInt32(); + if (underlyingType == typeof(ulong)) + return json.GetUInt64(); + if (underlyingType == typeof(decimal)) + return json.GetDecimal(); + + // Enums + if (underlyingType.IsEnum) + { + var enumString = json.GetString(); + if (enumString != null && Enum.TryParse(underlyingType, enumString, ignoreCase: true, out var enumValue)) + return enumValue; + if (json.ValueKind == JsonValueKind.Number) + return Enum.ToObject(underlyingType, json.GetInt32()); + throw new InvalidOperationException($"Cannot convert '{json}' to enum {underlyingType.Name}"); + } + + // Guid + if (underlyingType == typeof(Guid)) + return json.GetGuid(); + + // TimeSpan (accepts seconds as number, or string like "0:00:05") + if (underlyingType == typeof(TimeSpan)) + { + if (json.ValueKind == JsonValueKind.Number) + return TimeSpan.FromSeconds(json.GetDouble()); + if (json.ValueKind == JsonValueKind.String && TimeSpan.TryParse(json.GetString(), out var ts)) + return ts; + throw new InvalidOperationException($"Cannot convert '{json}' to TimeSpan. Use seconds (number) or \"h:mm:ss\" string."); + } + + // File/directory paths (UFile, UDirectory) + if (underlyingType == typeof(UFile)) + return new UFile(json.GetString()); + if (underlyingType == typeof(UDirectory)) + return new UDirectory(json.GetString()); + + // --- Float vector types --- + + if (underlyingType == typeof(Vector2) && json.ValueKind == JsonValueKind.Object) + { + return new Vector2( + GetFloat(json, "x", "X"), + GetFloat(json, "y", "Y")); + } + + if (underlyingType == typeof(Vector3) && json.ValueKind == JsonValueKind.Object) + { + return new Vector3( + GetFloat(json, "x", "X"), + GetFloat(json, "y", "Y"), + GetFloat(json, "z", "Z")); + } + + if (underlyingType == typeof(Vector4) && json.ValueKind == JsonValueKind.Object) + { + return new Vector4( + GetFloat(json, "x", "X"), + GetFloat(json, "y", "Y"), + GetFloat(json, "z", "Z"), + GetFloat(json, "w", "W")); + } + + // --- Integer vector types --- + + if (underlyingType == typeof(Int2) && json.ValueKind == JsonValueKind.Object) + { + return new Int2( + GetInt(json, "x", "X"), + GetInt(json, "y", "Y")); + } + + if (underlyingType == typeof(Int3) && json.ValueKind == JsonValueKind.Object) + { + return new Int3( + GetInt(json, "x", "X"), + GetInt(json, "y", "Y"), + GetInt(json, "z", "Z")); + } + + if (underlyingType == typeof(Int4) && json.ValueKind == JsonValueKind.Object) + { + return new Int4( + GetInt(json, "x", "X"), + GetInt(json, "y", "Y"), + GetInt(json, "z", "Z"), + GetInt(json, "w", "W")); + } + + // --- Quaternion --- + + if (underlyingType == typeof(Quaternion) && json.ValueKind == JsonValueKind.Object) + { + return new Quaternion( + GetFloat(json, "x", "X"), + GetFloat(json, "y", "Y"), + GetFloat(json, "z", "Z"), + GetFloat(json, "w", "W", 1f)); + } + + // --- Color types --- + + if (underlyingType == typeof(Color) && json.ValueKind == JsonValueKind.Object) + { + return new Color( + (byte)GetInt(json, "r", "R"), + (byte)GetInt(json, "g", "G"), + (byte)GetInt(json, "b", "B"), + (byte)GetInt(json, "a", "A", 255)); + } + + if (underlyingType == typeof(Color4) && json.ValueKind == JsonValueKind.Object) + { + return new Color4( + GetFloat(json, "r", "R"), + GetFloat(json, "g", "G"), + GetFloat(json, "b", "B"), + GetFloat(json, "a", "A", 1f)); + } + + if (underlyingType == typeof(Color3) && json.ValueKind == JsonValueKind.Object) + { + return new Color3( + GetFloat(json, "r", "R"), + GetFloat(json, "g", "G"), + GetFloat(json, "b", "B")); + } + + // --- Rectangle and size types --- + + if (underlyingType == typeof(RectangleF) && json.ValueKind == JsonValueKind.Object) + { + return new RectangleF( + GetFloat(json, "x", "X"), + GetFloat(json, "y", "Y"), + GetFloat(json, "width", "Width"), + GetFloat(json, "height", "Height")); + } + + if (underlyingType == typeof(Rectangle) && json.ValueKind == JsonValueKind.Object) + { + return new Rectangle( + GetInt(json, "x", "X"), + GetInt(json, "y", "Y"), + GetInt(json, "width", "Width"), + GetInt(json, "height", "Height")); + } + + if (underlyingType == typeof(Size2) && json.ValueKind == JsonValueKind.Object) + { + return new Size2( + GetInt(json, "width", "Width"), + GetInt(json, "height", "Height")); + } + + if (underlyingType == typeof(Size2F) && json.ValueKind == JsonValueKind.Object) + { + return new Size2F( + GetFloat(json, "width", "Width"), + GetFloat(json, "height", "Height")); + } + + if (underlyingType == typeof(Size3) && json.ValueKind == JsonValueKind.Object) + { + return new Size3( + GetInt(json, "width", "Width"), + GetInt(json, "height", "Height"), + GetInt(json, "depth", "Depth")); + } + + // --- Angle --- + + if (underlyingType == typeof(AngleSingle)) + { + if (json.ValueKind == JsonValueKind.Number) + return new AngleSingle(json.GetSingle(), AngleType.Degree); + if (json.ValueKind == JsonValueKind.Object) + { + if (json.TryGetProperty("degrees", out var deg) || json.TryGetProperty("Degrees", out deg)) + return new AngleSingle(deg.GetSingle(), AngleType.Degree); + if (json.TryGetProperty("radians", out var rad) || json.TryGetProperty("Radians", out rad)) + return new AngleSingle(rad.GetSingle(), AngleType.Radian); + } + throw new InvalidOperationException($"Cannot convert '{json}' to AngleSingle. Use a number (degrees), or {{\"degrees\": N}} / {{\"radians\": N}}."); + } + + // Polymorphic types (interfaces/abstract classes) — resolve concrete type and instantiate + if (IsPolymorphicType(underlyingType)) + return ConvertPolymorphicValue(json, underlyingType, session: null); + + // TypeConverter fallback — handles any type with a registered TypeConverter (e.g. custom Stride types) + var converter = TypeDescriptor.GetConverter(underlyingType); + if (converter.CanConvertFrom(typeof(string)) && json.ValueKind == JsonValueKind.String) + { + try + { + return converter.ConvertFromInvariantString(json.GetString()!); + } + catch (Exception ex) + { + throw new InvalidOperationException($"TypeConverter failed for {targetType.Name}: {ex.Message}", ex); + } + } + + throw new InvalidOperationException( + $"Cannot convert JSON value to type {targetType.Name}. " + + $"Supported types: all numeric primitives, bool, string, enum, Guid, TimeSpan, " + + $"UFile, UDirectory, Vector2/3/4, Int2/3/4, Quaternion, Color, Color3, Color4, " + + $"Rectangle, RectangleF, Size2, Size2F, Size3, AngleSingle, " + + $"asset references (use session overload), and any type with a TypeConverter."); + } + + // --- JSON property extraction helpers --- + + private static float GetFloat(JsonElement json, string name1, string name2, float defaultValue = 0f) + => json.TryGetProperty(name1, out var v) || json.TryGetProperty(name2, out v) ? v.GetSingle() : defaultValue; + + private static int GetInt(JsonElement json, string name1, string name2, int defaultValue = 0) + => json.TryGetProperty(name1, out var v) || json.TryGetProperty(name2, out v) ? v.GetInt32() : defaultValue; + + /// + /// Parses an asset ID from JSON and creates a proper asset reference via ContentReferenceHelper. + /// Accepted formats: {"assetId":"GUID"}, {"assetRef":"GUID"}, "GUID", or null. + /// + private static object? ConvertAssetReference(JsonElement json, Type targetType, SessionViewModel session) + { + // Parse asset ID from various JSON formats + string? assetIdStr = null; + + if (json.ValueKind == JsonValueKind.Object) + { + if (json.TryGetProperty("assetId", out var idProp)) + assetIdStr = idProp.GetString(); + else if (json.TryGetProperty("assetRef", out var refProp)) + assetIdStr = refProp.GetString(); + } + else if (json.ValueKind == JsonValueKind.String) + { + assetIdStr = json.GetString(); + } + + if (string.IsNullOrEmpty(assetIdStr)) + { + throw new InvalidOperationException("Asset reference JSON must contain an asset ID. Use {\"assetId\":\"GUID\"}, {\"assetRef\":\"GUID\"}, or \"GUID\"."); + } + + if (!AssetId.TryParse(assetIdStr, out var assetId)) + { + throw new InvalidOperationException($"Invalid asset ID format: '{assetIdStr}'. Expected a GUID."); + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm == null) + { + throw new InvalidOperationException($"Asset not found: '{assetIdStr}'. Use query_assets to find valid asset IDs."); + } + + var reference = ContentReferenceHelper.CreateReference(assetVm, targetType); + if (reference == null) + { + throw new InvalidOperationException($"Cannot create a reference of type {targetType.Name} to asset '{assetVm.Name}' ({assetVm.AssetItem.Asset.GetType().Name}). The asset type may not be compatible with this property."); + } + + return reference; + } + + /// + /// Returns true if the type is an interface or abstract class that requires polymorphic resolution. + /// + private static bool IsPolymorphicType(Type type) + => (type.IsInterface || type.IsAbstract) && type != typeof(string); + + /// + /// Resolves a user-provided type name to a concrete type that implements the given interface/abstract type. + /// Resolution order: DataContract alias, short class name, fully qualified name. + /// + private static Type? ResolveConcreteType(string typeName, Type targetType) + { + var concreteTypes = targetType.GetInheritedInstantiableTypes() + .Where(t => Attribute.GetCustomAttribute(t, typeof(NonInstantiableAttribute)) == null) + .ToList(); + + // 1. Match by DataContract alias + foreach (var type in concreteTypes) + { + var dc = type.GetCustomAttribute(false); + if (dc?.Alias != null && string.Equals(dc.Alias, typeName, StringComparison.OrdinalIgnoreCase)) + return type; + } + + // 2. Match by short class name + foreach (var type in concreteTypes) + { + if (string.Equals(type.Name, typeName, StringComparison.OrdinalIgnoreCase)) + return type; + } + + // 3. Match by fully qualified name + foreach (var type in concreteTypes) + { + if (string.Equals(type.FullName, typeName, StringComparison.OrdinalIgnoreCase)) + return type; + } + + return null; + } + + /// + /// Returns a comma-separated list of available concrete type names for error messages. + /// Uses DataContract alias when available, falls back to class name. + /// + private static string GetAvailableTypeNames(Type targetType) + { + var concreteTypes = targetType.GetInheritedInstantiableTypes() + .Where(t => Attribute.GetCustomAttribute(t, typeof(NonInstantiableAttribute)) == null) + .OrderBy(t => t.Name) + .ToList(); + + var names = concreteTypes.Select(t => + { + var dc = t.GetCustomAttribute(false); + return dc?.Alias ?? t.Name; + }); + + return string.Join(", ", names); + } + + /// + /// Converts a JSON value to a concrete instance of a polymorphic (interface/abstract) type. + /// Accepts two formats: a string type name, or an object with "$type" plus optional inline properties. + /// + private static object? ConvertPolymorphicValue(JsonElement json, Type targetType, SessionViewModel? session) + { + if (json.ValueKind == JsonValueKind.Null) + return null; + + string? typeName; + JsonElement? propertiesJson = null; + + if (json.ValueKind == JsonValueKind.String) + { + // Format 1: "LightPoint" + typeName = json.GetString(); + } + else if (json.ValueKind == JsonValueKind.Object) + { + // Format 2: {"$type": "LightPoint", "Radius": 5.0, ...} + if (!json.TryGetProperty("$type", out var typeElement)) + { + throw new InvalidOperationException( + $"Polymorphic value for '{targetType.Name}' must be a type name string or an object with a '$type' property. " + + $"Available types: {GetAvailableTypeNames(targetType)}"); + } + typeName = typeElement.GetString(); + propertiesJson = json; + } + else + { + throw new InvalidOperationException( + $"Polymorphic value for '{targetType.Name}' must be a type name string or an object with a '$type' property. " + + $"Available types: {GetAvailableTypeNames(targetType)}"); + } + + if (string.IsNullOrEmpty(typeName)) + { + throw new InvalidOperationException( + $"Type name cannot be empty for '{targetType.Name}'. " + + $"Available types: {GetAvailableTypeNames(targetType)}"); + } + + var concreteType = ResolveConcreteType(typeName, targetType); + if (concreteType == null) + { + throw new InvalidOperationException( + $"Type '{typeName}' not found for '{targetType.Name}'. " + + $"Available types: {GetAvailableTypeNames(targetType)}"); + } + + // Instantiate via ObjectFactoryRegistry (same as editor property grid) + object instance; + try + { + instance = ObjectFactoryRegistry.NewInstance(concreteType); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to create instance of '{concreteType.Name}' for '{targetType.Name}': {ex.Message}", ex); + } + + // Apply inline properties if object format was used + if (propertiesJson.HasValue) + { + ApplyInlineProperties(instance, propertiesJson.Value, session); + } + + return instance; + } + + /// + /// Applies inline properties from a JSON object to a newly created polymorphic instance. + /// Matches properties by name (case-insensitive) against [DataMember] fields and properties. + /// + private static void ApplyInlineProperties(object instance, JsonElement json, SessionViewModel? session) + { + var type = instance.GetType(); + + foreach (var jsonProp in json.EnumerateObject()) + { + // Skip the $type discriminator + if (jsonProp.Name == "$type") + continue; + + // Search for matching [DataMember] property + var prop = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(p => + p.GetCustomAttribute() != null && + string.Equals(p.Name, jsonProp.Name, StringComparison.OrdinalIgnoreCase) && + p.CanWrite); + + if (prop != null) + { + var value = session != null + ? ConvertJsonToType(jsonProp.Value, prop.PropertyType, session) + : ConvertJsonToType(jsonProp.Value, prop.PropertyType); + prop.SetValue(instance, value); + continue; + } + + // Search for matching [DataMember] field + var field = type.GetFields(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(f => + f.GetCustomAttribute() != null && + string.Equals(f.Name, jsonProp.Name, StringComparison.OrdinalIgnoreCase) && + !f.IsInitOnly); + + if (field != null) + { + var value = session != null + ? ConvertJsonToType(jsonProp.Value, field.FieldType, session) + : ConvertJsonToType(jsonProp.Value, field.FieldType); + field.SetValue(instance, value); + continue; + } + + // Silently skip unknown properties — the agent may pass extra metadata + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/ManageAssetTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/ManageAssetTool.cs new file mode 100644 index 0000000000..6b2696e6f2 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/ManageAssetTool.cs @@ -0,0 +1,190 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class ManageAssetTool +{ + [McpServerTool(Name = "manage_asset"), Description("Performs organizational operations on existing assets: rename, move, or delete. For delete, the tool checks for inbound references first and returns an error if the asset is still referenced (use get_asset_dependencies to check). All operations support undo/redo. Note: to reimport assets from source files, use build_project which automatically reimports all modified sources before building.")] + public static async Task ManageAsset( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID (GUID from query_assets)")] string assetId, + [Description("The action to perform: 'rename', 'move', or 'delete'")] string action, + [Description("For 'rename': the new name for the asset")] string? newName = null, + [Description("For 'move': the target directory path (e.g. 'Materials/Environment')")] string? newDirectory = null, + CancellationToken cancellationToken = default) + { + // Delete uses async UI operations, so handle it specially + if (action.Equals("delete", StringComparison.OrdinalIgnoreCase)) + { + return await HandleDelete(session, dispatcher, assetId, cancellationToken); + } + + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", result = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", result = (object?)null }; + } + + switch (action.ToLowerInvariant()) + { + case "rename": + return HandleRename(assetVm, newName); + case "move": + return HandleMove(session, assetVm, newDirectory); + default: + return new { error = $"Unknown action: '{action}'. Expected 'rename', 'move', or 'delete'.", result = (object?)null }; + } + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object HandleRename(AssetViewModel assetVm, string? newName) + { + if (string.IsNullOrWhiteSpace(newName)) + { + return new { error = "newName is required for 'rename' action.", result = (object?)null }; + } + + var oldName = assetVm.Name; + assetVm.Name = newName; + + return new + { + error = (string?)null, + result = (object)new + { + action = "renamed", + oldName, + newName = assetVm.Name, + url = assetVm.Url, + }, + }; + } + + private static object HandleMove(SessionViewModel session, AssetViewModel assetVm, string? newDirectory) + { + if (newDirectory == null) + { + return new { error = "newDirectory is required for 'move' action.", result = (object?)null }; + } + + var package = assetVm.Directory?.Package; + if (package == null) + { + return new { error = "Cannot determine the asset's package.", result = (object?)null }; + } + + var oldUrl = assetVm.Url; + var targetDir = package.GetOrCreateAssetDirectory(newDirectory, canUndoRedoCreation: true); + assetVm.MoveAsset(package.Package, targetDir); + + return new + { + error = (string?)null, + result = (object)new + { + action = "moved", + oldUrl, + newUrl = assetVm.Url, + newDirectory, + }, + }; + } + + private static async Task HandleDelete( + SessionViewModel session, + DispatcherBridge dispatcher, + string assetId, + CancellationToken cancellationToken) + { + var result = await dispatcher.InvokeTaskOnUIThread(async () => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", result = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", result = (object?)null }; + } + + // Check if asset can be deleted + if (!assetVm.CanDelete(out var deleteError)) + { + return new { error = $"Cannot delete asset: {deleteError}", result = (object?)null }; + } + + // Pre-check for inbound references to avoid the fix-references dialog + var deps = session.DependencyManager.ComputeDependencies(id, AssetDependencySearchOptions.In, ContentLinkType.Reference); + if (deps != null) + { + var referencers = deps.LinksIn + .Where(link => session.GetAssetById(link.Item.Id) != null) + .Select(link => + { + var refVm = session.GetAssetById(link.Item.Id); + return new { id = link.Item.Id.ToString(), name = refVm?.Name, type = refVm?.Asset.GetType().Name }; + }) + .ToList(); + + if (referencers.Count > 0) + { + return new + { + error = $"Cannot delete: asset is referenced by {referencers.Count} other asset(s). Use get_asset_dependencies to see the full list, and remove references first.", + result = (object)new { referencedBy = referencers }, + }; + } + } + + var assetName = assetVm.Name; + var assetType = assetVm.Asset.GetType().Name; + + var success = await session.DeleteItems(new object[] { assetVm }, skipConfirmation: true); + + if (!success) + { + return new { error = "Delete operation was cancelled or failed.", result = (object?)null }; + } + + return new + { + error = (string?)null, + result = (object)new + { + action = "deleted", + name = assetName, + type = assetType, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/ManageRootAssetsTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/ManageRootAssetsTool.cs new file mode 100644 index 0000000000..8338b16398 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/ManageRootAssetsTool.cs @@ -0,0 +1,179 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class ManageRootAssetsTool +{ + [McpServerTool(Name = "manage_root_assets"), Description("Manages which assets are included in the game build. Root assets (and their dependencies) are compiled when building. Use 'list' to see current root assets, 'add' to mark an asset for build inclusion, 'remove' to exclude it. Scenes must typically be added as root assets to be included in the built game.")] + public static async Task ManageRootAssets( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The action to perform: 'list', 'add', or 'remove'")] string action, + [Description("The asset ID (GUID) — required for 'add' and 'remove' actions")] string? assetId = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + switch (action.ToLowerInvariant()) + { + case "list": + return HandleList(session); + case "add": + return HandleAdd(session, assetId); + case "remove": + return HandleRemove(session, assetId); + default: + return new { error = $"Unknown action: '{action}'. Expected 'list', 'add', or 'remove'.", assets = (object?)null, result = (object?)null }; + } + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object HandleList(SessionViewModel session) + { + var currentProject = session.CurrentProject; + if (currentProject == null) + { + return new { error = "No active project.", assets = (object?)null, result = (object?)null }; + } + + var rootAssets = currentProject.RootAssets + .Where(a => a != null) + .Select(a => new + { + id = a.Id.ToString(), + name = a.Name, + type = a.AssetType?.Name ?? "Unknown", + url = a.Url, + }) + .ToList(); + + return new + { + error = (string?)null, + assets = (object?)rootAssets, + result = (object?)null, + }; + } + + private static object HandleAdd(SessionViewModel session, string? assetId) + { + if (string.IsNullOrWhiteSpace(assetId)) + { + return new { error = "assetId is required for 'add' action.", assets = (object?)null, result = (object?)null }; + } + + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", assets = (object?)null, result = (object?)null }; + } + + var currentProject = session.CurrentProject; + if (currentProject == null) + { + return new { error = "No active project.", assets = (object?)null, result = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", assets = (object?)null, result = (object?)null }; + } + + if (!currentProject.IsInScope(assetVm)) + { + return new { error = $"Asset '{assetVm.Name}' is not in scope for the current project '{currentProject.Name}'.", assets = (object?)null, result = (object?)null }; + } + + if (currentProject.RootAssets.Contains(assetVm)) + { + return new { error = $"Asset '{assetVm.Name}' is already a root asset.", assets = (object?)null, result = (object?)null }; + } + + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + assetVm.Dependencies.IsRoot = true; + undoRedoService.SetName(transaction, "Add root asset"); + } + + return new + { + error = (string?)null, + assets = (object?)null, + result = (object)new + { + action = "added", + id = assetVm.Id.ToString(), + name = assetVm.Name, + type = assetVm.AssetType.Name, + url = assetVm.Url, + }, + }; + } + + private static object HandleRemove(SessionViewModel session, string? assetId) + { + if (string.IsNullOrWhiteSpace(assetId)) + { + return new { error = "assetId is required for 'remove' action.", assets = (object?)null, result = (object?)null }; + } + + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", assets = (object?)null, result = (object?)null }; + } + + var currentProject = session.CurrentProject; + if (currentProject == null) + { + return new { error = "No active project.", assets = (object?)null, result = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", assets = (object?)null, result = (object?)null }; + } + + if (!currentProject.RootAssets.Contains(assetVm)) + { + return new { error = $"Asset '{assetVm.Name}' is not a root asset.", assets = (object?)null, result = (object?)null }; + } + + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + assetVm.Dependencies.IsRoot = false; + undoRedoService.SetName(transaction, "Remove root asset"); + } + + return new + { + error = (string?)null, + assets = (object?)null, + result = (object)new + { + action = "removed", + id = assetVm.Id.ToString(), + name = assetVm.Name, + type = assetVm.AssetType.Name, + url = assetVm.Url, + }, + }; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/ModifyComponentTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/ModifyComponentTool.cs new file mode 100644 index 0000000000..343726ecc1 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/ModifyComponentTool.cs @@ -0,0 +1,461 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.Core.Quantum; +using Stride.Core.Extensions; +using Stride.Core.Reflection; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class ModifyComponentTool +{ + [McpServerTool(Name = "modify_component"), Description("Adds, removes, or updates a component on an entity. The scene must be open in the editor (use open_scene first). Actions: 'add' creates a new component, 'remove' deletes a component by index, 'update' sets properties on a component by index. Supports bracket notation for dictionary/list entries (e.g. '{\"Animations[Idle]\":{\"assetId\":\"GUID\"}}') and whole-dictionary JSON objects. The TransformComponent (index 0) cannot be removed. This operation supports undo/redo in the editor. For 'update', asset reference properties (e.g. ModelComponent.Model, BackgroundComponent.Texture) can be set using {\"PropertyName\":{\"assetId\":\"GUID\"}} — use query_assets to find the asset ID. NOTE: User game script types require the project to be built and assemblies reloaded first (use build_project, then get_build_status to wait, then reload_assemblies).")] + public static async Task ModifyComponent( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene containing the entity")] string sceneId, + [Description("The entity ID to modify")] string entityId, + [Description("The action to perform: 'add', 'remove', or 'update'")] string action, + [Description("For 'add': the component type name (e.g. 'ModelComponent', 'Stride.Engine.LightComponent'). For 'remove'/'update': not required.")] string? componentType = null, + [Description("For 'remove'/'update': the zero-based index of the component in the entity's component list. Use get_entity to see component indices.")] int? componentIndex = null, + [Description("For 'update': JSON object of property names and values to set. Scalar: '{\"Intensity\":2.0,\"Enabled\":false}'. Asset references: '{\"Model\":{\"assetId\":\"GUID\"}}' or '{\"Model\":\"GUID\"}'. Use null to clear: '{\"Model\":null}'. Bracket notation for dict entries: '{\"Animations[Idle]\":{\"assetId\":\"GUID\"}}'. Whole dict as JSON object: '{\"Animations\":{\"Idle\":{\"assetId\":\"GUID1\"},\"Run\":{\"assetId\":\"GUID2\"}}}'.")] string? properties = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", component = (object?)null }; + } + + if (!Guid.TryParse(entityId, out var entityGuid)) + { + return new { error = "Invalid entity ID format. Expected a GUID.", component = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", component = (object?)null }; + } + + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + { + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", component = (object?)null }; + } + + var absoluteId = new AbsoluteId(assetId, entityGuid); + var partVm = editor.FindPartViewModel(absoluteId); + if (partVm is not EntityViewModel entityVm) + { + return new { error = $"Entity not found in scene: {entityId}", component = (object?)null }; + } + + var entity = entityVm.AssetSideEntity; + + switch (action.ToLowerInvariant()) + { + case "add": + return AddComponent(session, entity, componentType); + case "remove": + return RemoveComponent(session, entity, componentIndex); + case "update": + return UpdateComponent(session, entity, componentIndex, properties); + default: + return new { error = $"Unknown action: '{action}'. Expected 'add', 'remove', or 'update'.", component = (object?)null }; + } + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object AddComponent(SessionViewModel session, Entity entity, string? componentTypeName) + { + if (string.IsNullOrEmpty(componentTypeName)) + { + return new { error = "componentType is required for 'add' action.", component = (object?)null }; + } + + var resolvedType = ResolveComponentType(componentTypeName); + if (resolvedType == null) + { + var userTypesHint = GetAvailableUserComponentTypes(); + return new + { + error = $"Component type not found: '{componentTypeName}'. " + + "Built-in examples: ModelComponent, LightComponent, CameraComponent, BackgroundComponent, SpriteComponent, AudioEmitterComponent, RigidbodyComponent, CharacterComponent. " + + "User game script types (e.g. PlayerController) require the project to be built and assemblies reloaded — use `build_project`, then `get_build_status` to wait for completion, then `reload_assemblies` to load the new types. " + + "Also try the fully qualified type name (e.g. 'MyGame.PlayerController')." + + userTypesHint, + component = (object?)null, + }; + } + + // Validate singleton constraint + var attributes = EntityComponentAttributes.Get(resolvedType); + if (!attributes.AllowMultipleComponents) + { + if (entity.Components.Any(c => c.GetType() == resolvedType)) + { + return new { error = $"Entity already has a {resolvedType.Name} and this component type does not allow multiples.", component = (object?)null }; + } + } + + var entityNode = session.AssetNodeContainer.GetOrCreateNode(entity); + if (entityNode == null) + { + return new { error = "Failed to access entity node.", component = (object?)null }; + } + + var componentsNode = entityNode[nameof(Entity.Components)].Target; + if (componentsNode == null) + { + return new { error = "Failed to access entity components node.", component = (object?)null }; + } + + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + var newComponent = (EntityComponent)Activator.CreateInstance(resolvedType)!; + componentsNode.Add(newComponent); + undoRedoService.SetName(transaction, $"Add {resolvedType.Name}"); + } + + return new + { + error = (string?)null, + component = (object)new + { + action = "added", + type = resolvedType.Name, + index = entity.Components.Count - 1, + }, + }; + } + + private static object RemoveComponent(SessionViewModel session, Entity entity, int? componentIndex) + { + if (componentIndex == null) + { + return new { error = "componentIndex is required for 'remove' action.", component = (object?)null }; + } + + if (componentIndex == 0) + { + return new { error = "Cannot remove the TransformComponent (index 0). It is required on all entities.", component = (object?)null }; + } + + if (componentIndex < 0 || componentIndex >= entity.Components.Count) + { + return new { error = $"Component index {componentIndex} is out of range. Entity has {entity.Components.Count} components (indices 0-{entity.Components.Count - 1}).", component = (object?)null }; + } + + var componentToRemove = entity.Components[componentIndex.Value]; + var componentTypeName = componentToRemove.GetType().Name; + + var entityNode = session.AssetNodeContainer.GetOrCreateNode(entity); + if (entityNode == null) + { + return new { error = "Failed to access entity node.", component = (object?)null }; + } + + var componentsNode = entityNode[nameof(Entity.Components)].Target; + if (componentsNode == null) + { + return new { error = "Failed to access entity components node.", component = (object?)null }; + } + + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + componentsNode.Remove(componentToRemove, new NodeIndex(componentIndex.Value)); + undoRedoService.SetName(transaction, $"Remove {componentTypeName}"); + } + + return new + { + error = (string?)null, + component = (object)new + { + action = "removed", + type = componentTypeName, + index = componentIndex.Value, + }, + }; + } + + private static object UpdateComponent(SessionViewModel session, Entity entity, int? componentIndex, string? propertiesJson) + { + if (componentIndex == null) + { + return new { error = "componentIndex is required for 'update' action.", component = (object?)null }; + } + + if (string.IsNullOrEmpty(propertiesJson)) + { + return new { error = "properties JSON is required for 'update' action.", component = (object?)null }; + } + + if (componentIndex < 0 || componentIndex >= entity.Components.Count) + { + return new { error = $"Component index {componentIndex} is out of range. Entity has {entity.Components.Count} components (indices 0-{entity.Components.Count - 1}).", component = (object?)null }; + } + + Dictionary? propertiesToSet; + try + { + propertiesToSet = JsonSerializer.Deserialize>(propertiesJson); + } + catch (JsonException ex) + { + return new { error = $"Invalid properties JSON: {ex.Message}", component = (object?)null }; + } + + if (propertiesToSet == null || propertiesToSet.Count == 0) + { + return new { error = "Properties object is empty.", component = (object?)null }; + } + + var targetComponent = entity.Components[componentIndex.Value]; + var componentNode = session.AssetNodeContainer.GetOrCreateNode(targetComponent); + if (componentNode == null) + { + return new { error = "Failed to access component node.", component = (object?)null }; + } + + var updatedProperties = new List(); + var errors = new List(); + + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + foreach (var (propName, jsonValue) in propertiesToSet) + { + try + { + ParsePropertyName(propName, out var memberName, out var bracketKey); + + var memberNode = componentNode.TryGetChild(memberName); + if (memberNode == null) + { + errors.Add($"Property '{memberName}' not found on {targetComponent.GetType().Name}."); + continue; + } + + if (bracketKey != null) + { + // Bracket notation: "Animations[Idle]" → update single dict/list entry + UpdateIndexedProperty(memberNode, bracketKey, jsonValue, session); + } + else if (IsDictionaryType(memberNode.Type) && jsonValue.ValueKind == JsonValueKind.Object) + { + // Whole dictionary as JSON object + UpdateDictionaryFromJson(memberNode, jsonValue, session); + } + else + { + // Simple scalar update + var targetType = memberNode.Type; + var convertedValue = JsonTypeConverter.ConvertJsonToType(jsonValue, targetType, session); + memberNode.Update(convertedValue); + } + + updatedProperties.Add(propName); + } + catch (Exception ex) + { + errors.Add($"Failed to set '{propName}': {ex.Message}"); + } + } + + undoRedoService.SetName(transaction, $"Update {targetComponent.GetType().Name}"); + } + + return new + { + error = errors.Count > 0 ? string.Join("; ", errors) : (string?)null, + component = (object)new + { + action = "updated", + type = targetComponent.GetType().Name, + index = componentIndex.Value, + updatedProperties = updatedProperties.ToArray(), + }, + }; + } + + private static void ParsePropertyName(string propName, out string memberName, out string? bracketKey) + { + var bracketStart = propName.IndexOf('['); + if (bracketStart >= 0) + { + memberName = propName[..bracketStart]; + var bracketEnd = propName.IndexOf(']'); + bracketKey = bracketEnd > bracketStart + 1 ? propName[(bracketStart + 1)..bracketEnd] : null; + } + else + { + memberName = propName; + bracketKey = null; + } + } + + private static void UpdateIndexedProperty(IMemberNode memberNode, string bracketKey, JsonElement jsonValue, SessionViewModel session) + { + var target = memberNode.Target; + if (target == null) + throw new InvalidOperationException("Property has no target object — cannot set indexed value."); + + var descriptor = TypeDescriptorFactory.Default.Find(target.Type); + + Type valueType; + if (descriptor is DictionaryDescriptor dictDesc) + valueType = dictDesc.ValueType; + else if (descriptor is CollectionDescriptor collDesc) + valueType = collDesc.ElementType; + else + throw new InvalidOperationException($"Property type {target.Type.Name} does not support indexed access."); + + var nodeIndex = ResolveNodeIndex(target, bracketKey); + var convertedValue = JsonTypeConverter.ConvertJsonToType(jsonValue, valueType, session); + + // Check if the index already exists + bool exists = target.Indices != null && target.Indices.Any(idx => Equals(idx.Value, nodeIndex.Value)); + + if (exists) + target.Update(convertedValue, nodeIndex); + else + target.Add(convertedValue, nodeIndex); + } + + private static void UpdateDictionaryFromJson(IMemberNode memberNode, JsonElement jsonObject, SessionViewModel session) + { + var target = memberNode.Target; + if (target == null) + throw new InvalidOperationException("Property has no target object — cannot update dictionary."); + + var descriptor = TypeDescriptorFactory.Default.Find(target.Type); + if (descriptor is not DictionaryDescriptor dictDesc) + throw new InvalidOperationException($"Property type {target.Type.Name} is not a dictionary."); + + foreach (var entry in jsonObject.EnumerateObject()) + { + var key = ConvertDictionaryKey(entry.Name, dictDesc.KeyType); + var nodeIndex = new NodeIndex(key); + var convertedValue = JsonTypeConverter.ConvertJsonToType(entry.Value, dictDesc.ValueType, session); + + bool exists = target.Indices != null && target.Indices.Any(idx => Equals(idx.Value, nodeIndex.Value)); + + if (exists) + target.Update(convertedValue, nodeIndex); + else + target.Add(convertedValue, nodeIndex); + } + } + + private static NodeIndex ResolveNodeIndex(IObjectNode target, string bracketKey) + { + var descriptor = TypeDescriptorFactory.Default.Find(target.Type); + + if (descriptor is DictionaryDescriptor dictDesc) + { + var key = ConvertDictionaryKey(bracketKey, dictDesc.KeyType); + return new NodeIndex(key); + } + + // Collection/list: parse as integer index + if (int.TryParse(bracketKey, out var index)) + return new NodeIndex(index); + + throw new InvalidOperationException($"Cannot resolve index '{bracketKey}' — expected an integer for collection access."); + } + + private static object ConvertDictionaryKey(string key, Type keyType) + { + if (keyType == typeof(string)) + return key; + if (keyType == typeof(int) && int.TryParse(key, out var intKey)) + return intKey; + if (keyType.IsEnum && Enum.TryParse(keyType, key, ignoreCase: true, out var enumKey)) + return enumKey!; + + throw new InvalidOperationException($"Cannot convert '{key}' to dictionary key type {keyType.Name}."); + } + + private static bool IsDictionaryType(Type type) + => type.IsGenericType && type.GetInterface(typeof(IDictionary<,>).FullName!) != null; + + internal static Type? ResolveComponentType(string typeName) + { + // Use the same discovery mechanism as the editor's "Add component" dropdown: + // typeof(EntityComponent).GetInheritedInstantiableTypes() queries AssemblyRegistry + // which includes user game script assemblies loaded from the project. + var allComponentTypes = typeof(EntityComponent).GetInheritedInstantiableTypes(); + + // Try exact name match (short name like "ModelComponent") + var match = allComponentTypes.FirstOrDefault(t => t.Name == typeName); + if (match != null) + return match; + + // Try full name match (like "Stride.Engine.ModelComponent" or "MyGame.PlayerController") + match = allComponentTypes.FirstOrDefault(t => t.FullName == typeName); + if (match != null) + return match; + + // Try case-insensitive match + match = allComponentTypes.FirstOrDefault(t => + string.Equals(t.Name, typeName, StringComparison.OrdinalIgnoreCase)); + if (match != null) + return match; + + // Try case-insensitive full name match + match = allComponentTypes.FirstOrDefault(t => + string.Equals(t.FullName, typeName, StringComparison.OrdinalIgnoreCase)); + if (match != null) + return match; + + // Fallback: search AppDomain assemblies for types not registered with AssemblyRegistry + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var type = assembly.GetType(typeName, throwOnError: false); + if (type != null && typeof(EntityComponent).IsAssignableFrom(type)) + return type; + } + + return null; + } + + internal static string GetAvailableUserComponentTypes() + { + var allTypes = typeof(EntityComponent).GetInheritedInstantiableTypes(); + // Filter to non-Stride types (user scripts) + var userTypes = allTypes + .Where(t => t.Namespace != null && !t.Namespace.StartsWith("Stride.")) + .Select(t => t.FullName) + .OrderBy(n => n) + .ToArray(); + + if (userTypes.Length == 0) + return ""; + + return $" Available user script types: {string.Join(", ", userTypes)}."; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/NavigateViewportTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/NavigateViewportTool.cs new file mode 100644 index 0000000000..0fb2e707e2 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/NavigateViewportTool.cs @@ -0,0 +1,239 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Presentation.AssetEditors; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; +using Stride.Assets.Presentation.AssetEditors.GameEditor.Game; +using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; +using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Mathematics; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class NavigateViewportTool +{ + [McpServerTool(Name = "navigate_viewport"), Description("Controls the editor viewport camera. Actions: 'set_orientation' changes camera to a preset view (Front, Back, Top, Bottom, Left, Right). 'set_position' moves the camera to specific coordinates with optional yaw/pitch angles. 'set_projection' toggles between perspective and orthographic modes. 'set_field_of_view' changes the FOV (perspective) or orthographic size. 'get_camera_state' returns the current camera position, orientation, and projection settings. The scene must be open in the editor (use open_scene first).")] + public static async Task NavigateViewport( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene whose viewport camera to control")] string sceneId, + [Description("The action to perform: 'set_orientation', 'set_position', 'set_projection', 'set_field_of_view', 'get_camera_state'")] string action, + [Description("For 'set_orientation': one of 'Front', 'Back', 'Top', 'Bottom', 'Left', 'Right'")] string? orientation = null, + [Description("For 'set_position': X coordinate")] float? x = null, + [Description("For 'set_position': Y coordinate")] float? y = null, + [Description("For 'set_position': Z coordinate")] float? z = null, + [Description("For 'set_position': yaw angle in degrees (horizontal rotation, default ~45°)")] float? yaw = null, + [Description("For 'set_position': pitch angle in degrees (vertical rotation, default ~-15°)")] float? pitch = null, + [Description("For 'set_projection': true for orthographic, false for perspective")] bool? orthographic = null, + [Description("For 'set_field_of_view': FOV in degrees (perspective) or orthographic size")] float? value = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + return new { error = "Invalid scene ID format. Expected a GUID.", result = (object?)null }; + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + return new { error = $"Scene not found: {sceneId}", result = (object?)null }; + + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", result = (object?)null }; + + if (!editor.SceneInitialized) + return new { error = "Editor is still initializing. Please wait and try again.", result = (object?)null }; + + var cameraVmService = editor.GetEditorGameService(); + if (cameraVmService == null) + return new { error = "Camera service is not available for this editor.", result = (object?)null }; + + var entityCameraService = editor.GetEditorGameService(); + + switch (action.ToLowerInvariant()) + { + case "set_orientation": + return SetOrientation(cameraVmService, orientation); + case "set_position": + return SetPosition(cameraVmService, x, y, z, yaw, pitch); + case "set_projection": + return SetProjection(cameraVmService, orthographic); + case "set_field_of_view": + return SetFieldOfView(cameraVmService, entityCameraService, value); + case "get_camera_state": + return GetCameraState(cameraVmService, entityCameraService); + default: + return new { error = $"Unknown action: '{action}'. Expected 'set_orientation', 'set_position', 'set_projection', 'set_field_of_view', or 'get_camera_state'.", result = (object?)null }; + } + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object SetOrientation(IEditorGameCameraViewModelService cameraService, string? orientation) + { + if (string.IsNullOrEmpty(orientation)) + return new { error = "orientation parameter is required for 'set_orientation' action.", result = (object?)null }; + + if (!Enum.TryParse(orientation, ignoreCase: true, out var cameraOrientation)) + return new { error = $"Invalid orientation: '{orientation}'. Expected one of: Front, Back, Top, Bottom, Left, Right.", result = (object?)null }; + + cameraService.ResetCameraOrientation(cameraOrientation); + + return new + { + error = (string?)null, + result = (object)new + { + action = "set_orientation", + orientation = cameraOrientation.ToString(), + }, + }; + } + + private static object SetPosition(IEditorGameCameraViewModelService cameraService, float? x, float? y, float? z, float? yawDegrees, float? pitchDegrees) + { + if (x == null || y == null || z == null) + return new { error = "x, y, and z parameters are required for 'set_position' action.", result = (object?)null }; + + var snapshot = new Stride.Assets.Presentation.SceneEditor.SceneSettingsData(); + cameraService.SaveSettings(snapshot); + + snapshot.CamPosition = new Vector3(x.Value, y.Value, z.Value); + + if (yawDegrees.HasValue) + snapshot.CamPitchYaw = new Vector2(snapshot.CamPitchYaw.X, MathUtil.DegreesToRadians(yawDegrees.Value)); + + if (pitchDegrees.HasValue) + snapshot.CamPitchYaw = new Vector2(MathUtil.DegreesToRadians(pitchDegrees.Value), snapshot.CamPitchYaw.Y); + + cameraService.LoadSettings(snapshot); + + return new + { + error = (string?)null, + result = (object)new + { + action = "set_position", + position = new { x = x.Value, y = y.Value, z = z.Value }, + yawDegrees = MathUtil.RadiansToDegrees(snapshot.CamPitchYaw.Y), + pitchDegrees = MathUtil.RadiansToDegrees(snapshot.CamPitchYaw.X), + }, + }; + } + + private static object SetProjection(IEditorGameCameraViewModelService cameraService, bool? orthographic) + { + if (orthographic == null) + return new { error = "orthographic parameter is required for 'set_projection' action.", result = (object?)null }; + + cameraService.SetOrthographicProjection(orthographic.Value); + + return new + { + error = (string?)null, + result = (object)new + { + action = "set_projection", + projection = orthographic.Value ? "orthographic" : "perspective", + }, + }; + } + + private static object SetFieldOfView(IEditorGameCameraViewModelService cameraService, IEditorGameEntityCameraViewModelService? entityCameraService, float? value) + { + if (value == null) + return new { error = "value parameter is required for 'set_field_of_view' action.", result = (object?)null }; + + bool isOrthographic = entityCameraService?.Camera?.OrthographicProjection ?? false; + + if (isOrthographic) + { + cameraService.SetOrthographicSize(value.Value); + return new + { + error = (string?)null, + result = (object)new + { + action = "set_field_of_view", + projection = "orthographic", + orthographicSize = value.Value, + }, + }; + } + else + { + cameraService.SetFieldOfView(value.Value); + return new + { + error = (string?)null, + result = (object)new + { + action = "set_field_of_view", + projection = "perspective", + fieldOfViewDegrees = value.Value, + }, + }; + } + } + + private static object GetCameraState(IEditorGameCameraViewModelService cameraService, IEditorGameEntityCameraViewModelService? entityCameraService) + { + var snapshot = new Stride.Assets.Presentation.SceneEditor.SceneSettingsData(); + cameraService.SaveSettings(snapshot); + + bool isOrthographic = false; + float fov = 45f; + float nearPlane = 0.1f; + float farPlane = 1000f; + float orthographicSize = 10f; + float moveSpeed = EditorGameCameraService.DefaultMoveSpeed; + + if (entityCameraService != null) + { + var cam = entityCameraService.Camera; + isOrthographic = cam.OrthographicProjection; + fov = cam.FieldOfView; + nearPlane = cam.NearPlane; + farPlane = cam.FarPlane; + orthographicSize = cam.OrthographicSize; + moveSpeed = cam.MoveSpeed; + } + + float aspectRatio = 16f / 9f; + if (cameraService is IEditorGameCameraService gameCameraService) + { + aspectRatio = gameCameraService.AspectRatio; + } + + return new + { + error = (string?)null, + result = (object)new + { + action = "get_camera_state", + position = new { x = snapshot.CamPosition.X, y = snapshot.CamPosition.Y, z = snapshot.CamPosition.Z }, + yawDegrees = MathUtil.RadiansToDegrees(snapshot.CamPitchYaw.Y), + pitchDegrees = MathUtil.RadiansToDegrees(snapshot.CamPitchYaw.X), + projection = isOrthographic ? "orthographic" : "perspective", + fieldOfViewDegrees = fov, + orthographicSize, + nearPlane, + farPlane, + aspectRatio, + moveSpeed, + }, + }; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/OpenSceneTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/OpenSceneTool.cs new file mode 100644 index 0000000000..ab29355be3 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/OpenSceneTool.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Assets.Presentation.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class OpenSceneTool +{ + [McpServerTool(Name = "open_scene"), Description("Opens a scene asset in the editor. This will open the scene editor tab for the specified scene, allowing you to inspect and modify its entities. If the scene is already open, it will be activated/focused. Use get_editor_status or query_assets to find scene IDs first.")] + public static async Task OpenScene( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene to open (GUID from get_editor_status or query_assets)")] string sceneId, + CancellationToken cancellationToken = default) + { + // Validate and resolve the scene asset on the UI thread + var resolveResult = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return (Error: "Invalid scene ID format. Expected a GUID.", Asset: (SceneViewModel?)null); + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return (Error: $"Scene not found or asset is not a scene: {sceneId}", Asset: (SceneViewModel?)null); + } + + return (Error: (string?)null, Asset: sceneVm); + }, cancellationToken); + + if (resolveResult.Error != null) + { + return JsonSerializer.Serialize(new { error = resolveResult.Error }, new JsonSerializerOptions { WriteIndented = true }); + } + + var sceneVm = resolveResult.Asset!; + + // Open the asset editor window — this must happen on the UI thread + // OpenAssetEditorWindow is an async method that waits for the editor to load + await dispatcher.InvokeTaskOnUIThread(async () => + { + var editorsManager = session.ServiceProvider.Get(); + await editorsManager.OpenAssetEditorWindow(sceneVm); + }, cancellationToken); + + var result = await dispatcher.InvokeOnUIThread(() => + { + return new + { + status = "opened", + sceneId, + sceneName = sceneVm.Name, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/OpenUIPageTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/OpenUIPageTool.cs new file mode 100644 index 0000000000..3c1d8217ac --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/OpenUIPageTool.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Assets.Presentation.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class OpenUIPageTool +{ + [McpServerTool(Name = "open_ui_page"), Description("Opens a UI page asset in the editor. This will open the UI page editor tab, allowing you to inspect and modify its elements. If the page is already open, it will be activated/focused. Use query_assets with type 'UIPageAsset' to find UI page IDs.")] + public static async Task OpenUIPage( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the UI page to open (GUID from query_assets)")] string assetId, + CancellationToken cancellationToken = default) + { + var resolveResult = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return (Error: "Invalid asset ID format. Expected a GUID.", Asset: (UIPageViewModel?)null); + } + + var assetVm = session.GetAssetById(id); + if (assetVm is not UIPageViewModel uiPageVm) + { + return (Error: $"Asset not found or is not a UI page: {assetId}", Asset: (UIPageViewModel?)null); + } + + return (Error: (string?)null, Asset: uiPageVm); + }, cancellationToken); + + if (resolveResult.Error != null) + { + return JsonSerializer.Serialize(new { error = resolveResult.Error }, new JsonSerializerOptions { WriteIndented = true }); + } + + var uiPageVm = resolveResult.Asset!; + + await dispatcher.InvokeTaskOnUIThread(async () => + { + var editorsManager = session.ServiceProvider.Get(); + await editorsManager.OpenAssetEditorWindow(uiPageVm); + }, cancellationToken); + + var result = await dispatcher.InvokeOnUIThread(() => + { + return new + { + status = "opened", + assetId, + name = uiPageVm.Name, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/QueryAssetsTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/QueryAssetsTool.cs new file mode 100644 index 0000000000..3837872f96 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/QueryAssetsTool.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class QueryAssetsTool +{ + [McpServerTool(Name = "query_assets"), Description("Search and filter assets in the current project. Returns asset metadata including ID, name, type, and URL. Use filter for name substring matching, type for asset type filtering (e.g. 'SceneAsset', 'MaterialAsset', 'ModelAsset'), and folder for URL path prefix matching.")] + public static async Task QueryAssets( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("Optional name substring filter (case-insensitive)")] string? filter = null, + [Description("Optional asset type name filter (e.g. 'SceneAsset', 'MaterialAsset')")] string? type = null, + [Description("Optional URL path prefix filter")] string? folder = null, + [Description("Maximum number of results to return (default 100)")] int maxResults = 100, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + var query = session.AllAssets.AsEnumerable(); + + if (!string.IsNullOrEmpty(filter)) + { + query = query.Where(a => a.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrEmpty(type)) + { + query = query.Where(a => a.AssetType.Name.Equals(type, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrEmpty(folder)) + { + query = query.Where(a => a.Url != null && a.Url.StartsWith(folder, StringComparison.OrdinalIgnoreCase)); + } + + var assets = query + .Take(maxResults) + .Select(a => new + { + id = a.Id.ToString(), + name = a.Name, + type = a.AssetType.Name, + url = a.Url, + }) + .ToList(); + + return new + { + totalCount = session.AllAssets.Count(), + returnedCount = assets.Count, + assets, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/ReloadAssembliesTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/ReloadAssembliesTool.cs new file mode 100644 index 0000000000..220f7b1ab6 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/ReloadAssembliesTool.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Commands; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class ReloadAssembliesTool +{ + [McpServerTool(Name = "reload_assemblies"), Description("Reloads game assemblies after a build, making user-defined script types (e.g. PlayerController, EnemyAI) available for use in modify_component. This is the MCP equivalent of clicking the blinking 'Reload game assemblies' button in the toolbar. Call this after build_project completes successfully — without it, newly built script types won't be discoverable. Actions: 'status' checks if a reload is pending, 'reload' triggers the reload. After reload, user script types become available in modify_component's 'add' action.")] + public static async Task ReloadAssemblies( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The action to perform: 'status' (check if reload is pending) or 'reload' (trigger assembly reload)")] string action = "reload", + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeTaskOnUIThread(async () => + { + // Access DebuggingViewModel via reflection (it lives in Stride.GameStudio which we don't reference) + var editorVm = EditorViewModel.Instance; + if (editorVm == null) + { + return new { error = "EditorViewModel is not available.", result = (object?)null }; + } + + var debuggingProp = editorVm.GetType().GetProperty("Debugging"); + if (debuggingProp == null) + { + return new { error = "Debugging property not found on the editor view model.", result = (object?)null }; + } + + var debugging = debuggingProp.GetValue(editorVm); + if (debugging == null) + { + return new { error = "DebuggingViewModel is null — editor may still be initializing.", result = (object?)null }; + } + + var reloadCommandProp = debugging.GetType().GetProperty("ReloadAssembliesCommand"); + if (reloadCommandProp == null) + { + return new { error = "ReloadAssembliesCommand not found on DebuggingViewModel.", result = (object?)null }; + } + + var reloadCommand = reloadCommandProp.GetValue(debugging) as ICommandBase; + if (reloadCommand == null) + { + return new { error = "ReloadAssembliesCommand is not an ICommandBase.", result = (object?)null }; + } + + bool isPending = reloadCommand.IsEnabled; + + switch (action.ToLowerInvariant()) + { + case "status": + return new + { + error = (string?)null, + result = (object)new + { + assemblyReloadPending = isPending, + message = isPending + ? "Game assemblies have changed. Call reload_assemblies with action 'reload' to load the updated scripts." + : "No assembly reload pending. Assemblies are up to date.", + }, + }; + + case "reload": + if (!isPending) + { + return new + { + error = (string?)null, + result = (object)new + { + assemblyReloadPending = false, + message = "No assembly reload pending. Assemblies are already up to date.", + }, + }; + } + + // Execute the reload command — this is an async command that rebuilds if needed, + // then unloads old assemblies, loads new ones, and re-analyzes entity components. + reloadCommand.Execute(); + + // The command runs asynchronously via AnonymousTaskCommand. + // Wait briefly for it to start, then check if it's still enabled (pending). + // The reload itself may take several seconds for large projects. + await Task.Delay(500, cancellationToken); + + // After execution, check if the reload completed + bool stillPending = reloadCommand.IsEnabled; + + return new + { + error = (string?)null, + result = (object)new + { + assemblyReloadPending = stillPending, + message = stillPending + ? "Assembly reload initiated but still in progress. Check status again shortly." + : "Game assemblies reloaded successfully. User script types are now available.", + }, + }; + + default: + return new + { + error = $"Unknown action: '{action}'. Expected 'status' or 'reload'.", + result = (object?)null, + }; + } + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + /// + /// Helper to check if assembly reload is pending, usable from other tools. + /// + internal static bool IsReloadPending() + { + var editorVm = EditorViewModel.Instance; + if (editorVm == null) return false; + + var debuggingProp = editorVm.GetType().GetProperty("Debugging"); + if (debuggingProp == null) return false; + + var debugging = debuggingProp.GetValue(editorVm); + if (debugging == null) return false; + + var reloadCommandProp = debugging.GetType().GetProperty("ReloadAssembliesCommand"); + if (reloadCommandProp == null) return false; + + var reloadCommand = reloadCommandProp.GetValue(debugging) as ICommandBase; + return reloadCommand?.IsEnabled ?? false; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/ReloadSceneTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/ReloadSceneTool.cs new file mode 100644 index 0000000000..ff3fc90b6c --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/ReloadSceneTool.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Assets.Presentation.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class ReloadSceneTool +{ + [McpServerTool(Name = "reload_scene"), Description("Closes and reopens a scene editor tab to refresh its in-memory state. Useful after build_project completes (to pick up new script component types) or when the scene editor appears stale. WARNING: Any unsaved in-editor changes to this scene will be discarded. Save first with save_project if you want to keep changes.")] + public static async Task ReloadScene( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene to reload")] string sceneId, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeTaskOnUIThread(async () => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", result = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", result = (object?)null }; + } + + var editorsManager = session.ServiceProvider.Get(); + + // Close the scene editor without saving + var closed = editorsManager.CloseAssetEditorWindow(sceneVm, save: false); + if (!closed) + { + return new { error = "Failed to close the scene editor tab.", result = (object?)null }; + } + + // Reopen the scene editor + await editorsManager.OpenAssetEditorWindow(sceneVm); + + return new + { + error = (string?)null, + result = (object)new + { + status = "reloaded", + sceneId = sceneVm.Id.ToString(), + sceneName = sceneVm.Name, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/RemoveSpriteFrameTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/RemoveSpriteFrameTool.cs new file mode 100644 index 0000000000..4b95a9fbd9 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/RemoveSpriteFrameTool.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Sprite; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.Core.Quantum; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class RemoveSpriteFrameTool +{ + [McpServerTool(Name = "remove_sprite_frame"), Description("Removes a sprite frame (SpriteInfo) from a SpriteSheetAsset by index. This operation supports undo/redo.")] + public static async Task RemoveSpriteFrame( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the SpriteSheetAsset")] string assetId, + [Description("0-based index of the sprite frame to remove")] int index, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", removed = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", removed = (object?)null }; + } + + if (assetVm.Asset is not SpriteSheetAsset spriteSheet) + { + return new { error = $"Asset is not a SpriteSheetAsset: {assetVm.Name} ({assetVm.AssetType.Name})", removed = (object?)null }; + } + + if (index < 0 || index >= spriteSheet.Sprites.Count) + { + return new { error = $"Index {index} is out of range. Sprites collection has {spriteSheet.Sprites.Count} items (valid range: 0-{spriteSheet.Sprites.Count - 1}).", removed = (object?)null }; + } + + var spriteInfo = spriteSheet.Sprites[index]; + var frameName = spriteInfo.Name; + + // Get the property graph node for the Sprites collection + var rootNode = assetVm.PropertyGraph?.RootNode; + if (rootNode == null) + { + return new { error = "Cannot access property graph for this asset.", removed = (object?)null }; + } + + var spritesMember = rootNode.TryGetChild(nameof(SpriteSheetAsset.Sprites)); + if (spritesMember?.Target == null) + { + return new { error = "Cannot access Sprites collection in property graph.", removed = (object?)null }; + } + + var spritesNode = spritesMember.Target; + + // Remove within undo/redo transaction + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + spritesNode.Remove(spriteInfo, new NodeIndex(index)); + undoRedoService.SetName(transaction, $"Remove sprite frame '{frameName}'"); + } + + return new + { + error = (string?)null, + removed = (object)new + { + index, + name = frameName, + remainingCount = spriteSheet.Sprites.Count, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/RemoveUIElementTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/RemoveUIElementTool.cs new file mode 100644 index 0000000000..908538f189 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/RemoveUIElementTool.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Presentation.ViewModel; +using Stride.Assets.UI; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.UI; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class RemoveUIElementTool +{ + [McpServerTool(Name = "remove_ui_element"), Description("Removes a UI element and its descendants from a UIPageAsset. This operation supports undo/redo.")] + public static async Task RemoveUIElement( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the UIPageAsset")] string assetId, + [Description("The UI element ID to remove (GUID from get_ui_tree)")] string elementId, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", removed = (object?)null }; + } + + if (!Guid.TryParse(elementId, out var elementGuid)) + { + return new { error = "Invalid element ID format. Expected a GUID.", removed = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm is not UIBaseViewModel uiPageVm) + { + var errorMsg = assetVm == null + ? $"Asset not found: {assetId}" + : $"Asset is not a UI page: {assetVm.Name} ({assetVm.AssetType.Name})"; + return new { error = errorMsg, removed = (object?)null }; + } + + var uiAsset = (UIAssetBase)uiPageVm.Asset; + + if (!uiAsset.Hierarchy.Parts.TryGetValue(elementGuid, out var design)) + { + return new { error = $"UI element not found: {elementId}", removed = (object?)null }; + } + + var element = design.UIElement; + var elementName = element.Name ?? element.GetType().Name; + + // Count descendants for reporting + int descendantCount = CountDescendants(element); + + // Remove via the property graph with undo/redo support + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + uiPageVm.AssetHierarchyPropertyGraph.RemovePartFromAsset(design); + undoRedoService.SetName(transaction, $"Remove UI element '{elementName}'"); + } + + return new + { + error = (string?)null, + removed = (object)new + { + id = elementId, + name = elementName, + descendantsRemoved = descendantCount, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static int CountDescendants(UIElement element) + { + int count = 0; + foreach (var child in element.VisualChildren) + { + count += 1 + CountDescendants(child); + } + return count; + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/ReparentEntityTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/ReparentEntityTool.cs new file mode 100644 index 0000000000..b4b8a79403 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/ReparentEntityTool.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Entities; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Assets.Quantum; +using Stride.Core.Presentation.Services; +using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class ReparentEntityTool +{ + [McpServerTool(Name = "reparent_entity"), Description("Changes the parent of an entity in the scene hierarchy. The scene must be open in the editor (use open_scene first). Set newParentId to null/empty to move the entity to the root level. This operation supports undo/redo in the editor.")] + public static async Task ReparentEntity( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene containing the entity")] string sceneId, + [Description("The entity ID to reparent")] string entityId, + [Description("The new parent entity ID, or omit/null to move to root level")] string? newParentId = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", reparented = (object?)null }; + } + + if (!Guid.TryParse(entityId, out var entityGuid)) + { + return new { error = "Invalid entity ID format. Expected a GUID.", reparented = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", reparented = (object?)null }; + } + + // Get the editor for this scene + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + { + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", reparented = (object?)null }; + } + + // Find the entity to reparent + var absoluteId = new AbsoluteId(assetId, entityGuid); + var partVm = editor.FindPartViewModel(absoluteId); + if (partVm is not EntityViewModel entityVm) + { + return new { error = $"Entity not found in scene: {entityId}", reparented = (object?)null }; + } + + // Resolve new parent (if specified) + Entity? newParentEntity = null; + if (!string.IsNullOrEmpty(newParentId)) + { + if (!Guid.TryParse(newParentId, out var newParentGuid)) + { + return new { error = $"Invalid new parent entity ID format: {newParentId}", reparented = (object?)null }; + } + + var newParentAbsId = new AbsoluteId(assetId, newParentGuid); + var newParentPartVm = editor.FindPartViewModel(newParentAbsId); + if (newParentPartVm is not EntityViewModel newParentEntityVm) + { + return new { error = $"New parent entity not found: {newParentId}", reparented = (object?)null }; + } + + // Prevent circular parenting: check if the new parent is a descendant of the entity + var currentParent = newParentEntityVm.TransformParent; + while (currentParent != null) + { + if (currentParent is EntityViewModel pvm && pvm.AssetSideEntity.Id == entityGuid) + { + return new { error = "Cannot reparent: new parent is a descendant of the entity (circular reference).", reparented = (object?)null }; + } + currentParent = currentParent.TransformParent; + } + + newParentEntity = newParentEntityVm.AssetSideEntity; + } + + var entityName = entityVm.Name; + var entityDesign = ((IEditorDesignPartViewModel)entityVm).PartDesign; + var entity = entityVm.AssetSideEntity; + + // Perform reparent: clone → remove → re-add + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + // Clone the sub-hierarchy before removal + var hierarchy = AssetCompositeHierarchyPropertyGraph.CloneSubHierarchies( + session.AssetNodeContainer, + sceneVm.Asset, + new[] { entity.Id }, + SubHierarchyCloneFlags.None, + out _); + + // Remove from current position + sceneVm.AssetHierarchyPropertyGraph.RemovePartFromAsset(entityDesign); + + // Add to new position + var movedEntityDesign = hierarchy.Parts[entity.Id]; + int insertIndex = newParentEntity?.Transform.Children.Count + ?? sceneVm.Asset.Hierarchy.RootParts.Count; + + sceneVm.AssetHierarchyPropertyGraph.AddPartToAsset( + hierarchy.Parts, + movedEntityDesign, + newParentEntity, + insertIndex); + + undoRedoService.SetName(transaction, $"Reparent entity '{entityName}'"); + } + + return new + { + error = (string?)null, + reparented = (object)new + { + id = entityId, + name = entityName, + newParentId = newParentEntity?.Id.ToString(), + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/RestartGameStudioTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/RestartGameStudioTool.cs new file mode 100644 index 0000000000..a2322efb51 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/RestartGameStudioTool.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Commands; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class RestartGameStudioTool +{ + [McpServerTool(Name = "restart_game_studio"), Description("Restarts Game Studio to reload the entire project from disk. This is a destructive operation — the MCP connection will be lost and the client must reconnect to the new instance. Use this only when external tools have modified .csproj, .sln, or other project-level files that Game Studio needs to re-read. Any unsaved changes are automatically saved before restarting. For reloading game scripts after a build, use reload_assemblies instead — it's faster and doesn't drop the connection.")] + public static async Task RestartGameStudio( + SessionViewModel session, + DispatcherBridge dispatcher, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeTaskOnUIThread(async () => + { + var editorVm = EditorViewModel.Instance; + if (editorVm == null) + { + return new { error = "EditorViewModel is not available.", result = (object?)null }; + } + + // GameStudioViewModel has a ReloadSessionCommand property — access via reflection + // since we don't directly reference the Stride.GameStudio assembly + var reloadProp = editorVm.GetType().GetProperty("ReloadSessionCommand"); + if (reloadProp == null) + { + return new { error = "ReloadSessionCommand not found on the editor view model.", result = (object?)null }; + } + + var command = reloadProp.GetValue(editorVm) as ICommandBase; + if (command == null) + { + return new { error = "ReloadSessionCommand is not an ICommandBase.", result = (object?)null }; + } + + // Auto-save before restarting to prevent data loss + await session.SaveSession(); + + // Fire the reload command — this will trigger the close/restart sequence asynchronously. + command.Execute(); + + return new + { + error = (string?)null, + result = (object)new + { + status = "restart_initiated", + message = "Changes saved. Game Studio is restarting. The MCP connection will be lost. Reconnect to the new instance after restart.", + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/SaveProjectTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/SaveProjectTool.cs new file mode 100644 index 0000000000..f39dec79b8 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/SaveProjectTool.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class SaveProjectTool +{ + [McpServerTool(Name = "save_project"), Description("Saves all pending changes (scenes, entities, components, assets, etc.) to disk. Automatically reimports any assets whose source files have changed on disk before saving. Use this to persist your work — changes made through MCP tools are held in memory until saved. Note: build_project and restart_game_studio auto-save, so you don't need to call this before those. Use save_project when you want to checkpoint your work, or before external tools need to read the on-disk files. WARNING: This writes the editor's in-memory state to disk and will overwrite any external changes made to asset files outside of Game Studio.")] + public static async Task SaveProject( + SessionViewModel session, + DispatcherBridge dispatcher, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeTaskOnUIThread(async () => + { + var reimported = await AssetReimportHelper.ReimportModifiedAssets(session); + + var success = await session.SaveSession(); + + return new + { + error = success ? (string?)null : "Save failed. Check the editor log for details.", + result = (object)new + { + status = success ? "saved" : "failed", + reimportedAssets = reimported.Count > 0 ? reimported : null, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/SelectEntityTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/SelectEntityTool.cs new file mode 100644 index 0000000000..f621814873 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/SelectEntityTool.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class SelectEntityTool +{ + [McpServerTool(Name = "select_entity"), Description("Selects one or more entities in the scene editor's hierarchy. The scene must already be open in the editor (use open_scene first). Selecting an entity will highlight it in the scene tree and show its properties in the property grid.")] + public static async Task SelectEntity( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene containing the entity")] string sceneId, + [Description("The entity ID (GUID from get_scene_tree) to select. For multiple entities, separate IDs with commas.")] string entityId, + [Description("If true, add to existing selection instead of replacing it (default: false)")] bool addToSelection = false, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", selected = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", selected = (object?)null }; + } + + // Get the editor for this scene + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + { + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", selected = (object?)null }; + } + + // Parse entity IDs (support comma-separated) + var entityIdStrings = entityId.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var entityViewModels = new System.Collections.Generic.List(); + var errors = new System.Collections.Generic.List(); + + foreach (var idStr in entityIdStrings) + { + if (!Guid.TryParse(idStr, out var entityGuid)) + { + errors.Add($"Invalid entity ID format: {idStr}"); + continue; + } + + var absoluteId = new AbsoluteId(assetId, entityGuid); + var partVm = editor.FindPartViewModel(absoluteId); + if (partVm is EntityViewModel entityVm) + { + entityViewModels.Add(entityVm); + } + else + { + errors.Add($"Entity not found in scene: {idStr}"); + } + } + + if (entityViewModels.Count == 0) + { + var errorMsg = errors.Count > 0 + ? string.Join("; ", errors) + : "No valid entity IDs provided."; + return new { error = errorMsg, selected = (object?)null }; + } + + // Perform the selection + if (!addToSelection) + { + editor.ClearSelection(); + } + + foreach (var entityVm in entityViewModels) + { + editor.SelectedContent.Add(entityVm); + } + + var selectedInfo = entityViewModels + .Select(e => new { id = e.AssetSideEntity.Id.ToString(), name = e.Name }) + .ToList(); + + return new + { + error = (string?)null, + selected = (object)new + { + count = selectedInfo.Count, + entities = selectedInfo, + warnings = errors.Count > 0 ? errors : null, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/SetActiveProjectTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/SetActiveProjectTool.cs new file mode 100644 index 0000000000..1dedc4eeb1 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/SetActiveProjectTool.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class SetActiveProjectTool +{ + [McpServerTool(Name = "set_active_project"), Description("Changes which project is active in the editor. The active project determines which project is built and run. IMPORTANT: You should almost always select an Executable project (not a Library). Library projects contain shared assets but cannot be launched. Use get_editor_status to see available projects — look for ones with isExecutable=true.")] + public static async Task SetActiveProject( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("Name of the project to activate (case-insensitive)")] string projectName, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + var projects = session.LocalPackages + .OfType() + .ToList(); + + var projectVm = projects + .FirstOrDefault(p => p.Name.Equals(projectName, StringComparison.OrdinalIgnoreCase)); + + if (projectVm == null) + { + var availableNames = projects.Select(p => p.Name).ToArray(); + return new + { + error = $"Project not found: '{projectName}'. Available projects: {string.Join(", ", availableNames)}", + warning = (string?)null, + project = (object?)null, + }; + } + + // Execute the set current project command + session.SetCurrentProjectCommand.Execute(projectVm); + + var warning = projectVm.Type != ProjectType.Executable + ? "Warning: You selected a Library project. Library projects cannot be built into a runnable game. Consider selecting an Executable project instead." + : null; + + return new + { + error = (string?)null, + warning, + project = (object)new + { + name = projectVm.Name, + type = projectVm.Type.ToString(), + platform = projectVm.Platform.ToString(), + isCurrentProject = projectVm.IsCurrentProject, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/SetAssetPropertyTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/SetAssetPropertyTool.cs new file mode 100644 index 0000000000..dbcf96318a --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/SetAssetPropertyTool.cs @@ -0,0 +1,255 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.Core.Quantum; +using Stride.Core.Reflection; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class SetAssetPropertyTool +{ + [McpServerTool(Name = "set_asset_property"), Description("Sets a property on an asset using a dot-notation path through the property graph. Use get_asset_details to discover available property names. Supports nested paths (e.g. 'Attributes.CullMode'), list indexing (e.g. 'Layers[0].DiffuseModel'), and dictionary key access (e.g. 'Dict[key]'). When a path segment is invalid, the error lists available property names at that level. Supports undo/redo. Asset reference properties can be set using {\"assetId\":\"GUID\"} or just \"GUID\" — use query_assets to find valid asset IDs.")] + public static async Task SetAssetProperty( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID (GUID from query_assets)")] string assetId, + [Description("Dot-notation property path (e.g. 'Width', 'Attributes.CullMode', 'Layers[0].DiffuseModel', 'Dict[key]')")] string propertyPath, + [Description("JSON value to set. Scalar: '2048', 'true', '\"Back\"'. Color: '{\"r\":1,\"g\":0,\"b\":0,\"a\":1}'. Asset reference: '{\"assetId\":\"GUID\"}' or '\"GUID\"'. Clear reference: 'null'.")] string value, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", result = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", result = (object?)null }; + } + + var rootNode = assetVm.PropertyGraph?.RootNode; + if (rootNode == null) + { + return new { error = "Cannot access property graph for this asset.", result = (object?)null }; + } + + // Parse the JSON value + JsonElement jsonValue; + try + { + jsonValue = JsonSerializer.Deserialize(value); + } + catch (JsonException ex) + { + return new { error = $"Invalid JSON value: {ex.Message}", result = (object?)null }; + } + + // Navigate the property path + var segments = propertyPath.Split('.'); + IObjectNode currentObject = rootNode; + IMemberNode? leafMember = null; + string? leafBracketKey = null; + + for (int i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + + // Parse bracket notation: "Name[key]" or "Name[0]" + ParseBracket(segment, out var memberName, out var bracketKey); + + var member = currentObject.TryGetChild(memberName); + if (member == null) + { + var availableMembers = currentObject.Members.Select(m => m.Name).OrderBy(n => n).ToArray(); + return new + { + error = $"Property '{memberName}' not found at path level {i}. Available properties: {string.Join(", ", availableMembers)}", + result = (object?)null, + }; + } + + bool isLastSegment = i == segments.Length - 1; + + if (isLastSegment && bracketKey == null) + { + // Simple leaf property + leafMember = member; + } + else if (isLastSegment && bracketKey != null) + { + // Last segment with bracket — set indexed value (e.g. "Dict[key]" or "List[0]") + leafMember = member; + leafBracketKey = bracketKey; + } + else + { + // Navigate deeper + var target = member.Target; + if (bracketKey != null && target != null) + { + try + { + var nodeIndex = ResolveNodeIndex(target, bracketKey); + target = target.IndexedTarget(nodeIndex); + } + catch (Exception ex) + { + return new + { + error = $"Cannot resolve index [{bracketKey}] for property '{memberName}': {ex.Message}", + result = (object?)null, + }; + } + } + + if (target == null) + { + return new + { + error = $"Cannot navigate into property '{memberName}' — it has no target object (value may be null).", + result = (object?)null, + }; + } + + currentObject = target; + } + } + + if (leafMember == null) + { + return new { error = "Could not resolve property path.", result = (object?)null }; + } + + // Apply in undo/redo transaction + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + try + { + if (leafBracketKey != null) + { + // Set indexed value (dictionary or collection entry) + SetIndexedValue(leafMember, leafBracketKey, jsonValue, session); + } + else + { + // Simple property update + var convertedValue = JsonTypeConverter.ConvertJsonToType(jsonValue, leafMember.Type, session); + leafMember.Update(convertedValue); + } + } + catch (Exception ex) + { + return new { error = $"Cannot set value: {ex.Message}", result = (object?)null }; + } + + undoRedoService.SetName(transaction, $"Set {propertyPath} on '{assetVm.Name}'"); + } + + return new + { + error = (string?)null, + result = (object)new + { + assetId = assetVm.Id.ToString(), + assetName = assetVm.Name, + propertyPath, + newValueType = leafMember.Type.Name, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static void ParseBracket(string segment, out string memberName, out string? bracketKey) + { + var bracketStart = segment.IndexOf('['); + if (bracketStart >= 0) + { + memberName = segment[..bracketStart]; + var bracketEnd = segment.IndexOf(']'); + bracketKey = bracketEnd > bracketStart + 1 ? segment[(bracketStart + 1)..bracketEnd] : null; + } + else + { + memberName = segment; + bracketKey = null; + } + } + + private static NodeIndex ResolveNodeIndex(IObjectNode target, string bracketKey) + { + var descriptor = TypeDescriptorFactory.Default.Find(target.Type); + + if (descriptor is DictionaryDescriptor dictDesc) + { + var keyType = dictDesc.KeyType; + if (keyType == typeof(string)) + return new NodeIndex(bracketKey); + if (keyType == typeof(int) && int.TryParse(bracketKey, out var intKey)) + return new NodeIndex(intKey); + if (keyType.IsEnum && Enum.TryParse(keyType, bracketKey, ignoreCase: true, out var enumKey)) + return new NodeIndex(enumKey!); + + throw new InvalidOperationException($"Cannot convert '{bracketKey}' to dictionary key type {keyType.Name}."); + } + + // Collection/list: parse as integer index + if (int.TryParse(bracketKey, out var index)) + return new NodeIndex(index); + + throw new InvalidOperationException($"Cannot resolve index '{bracketKey}' — expected an integer for collection access."); + } + + private static void SetIndexedValue(IMemberNode memberNode, string bracketKey, JsonElement jsonValue, SessionViewModel session) + { + var target = memberNode.Target; + if (target == null) + throw new InvalidOperationException("Property has no target object — cannot set indexed value."); + + var nodeIndex = ResolveNodeIndex(target, bracketKey); + var descriptor = TypeDescriptorFactory.Default.Find(target.Type); + + // Determine value type + Type valueType; + if (descriptor is DictionaryDescriptor dictDesc) + valueType = dictDesc.ValueType; + else if (descriptor is CollectionDescriptor collDesc) + valueType = collDesc.ElementType; + else + throw new InvalidOperationException($"Property type {target.Type.Name} does not support indexed access."); + + var convertedValue = JsonTypeConverter.ConvertJsonToType(jsonValue, valueType, session); + + // Check if the index already exists + bool exists = false; + if (target.Indices != null) + { + exists = target.Indices.Any(idx => Equals(idx.Value, nodeIndex.Value)); + } + + if (exists) + { + target.Update(convertedValue, nodeIndex); + } + else + { + target.Add(convertedValue, nodeIndex); + } + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/SetTransformTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/SetTransformTool.cs new file mode 100644 index 0000000000..bfd84481b4 --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/SetTransformTool.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels; +using Stride.Assets.Presentation.ViewModel; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Mathematics; +using Stride.Core.Presentation.Services; +using Stride.Engine; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class SetTransformTool +{ + [McpServerTool(Name = "set_transform"), Description("Sets the transform (position, rotation, scale) of an entity. The scene must be open in the editor (use open_scene first). Only the provided components are changed; omitted ones keep their current values. Rotation uses Euler angles in degrees. This operation supports undo/redo in the editor.")] + public static async Task SetTransform( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the scene containing the entity")] string sceneId, + [Description("The entity ID to modify")] string entityId, + [Description("Position X coordinate")] float? positionX = null, + [Description("Position Y coordinate")] float? positionY = null, + [Description("Position Z coordinate")] float? positionZ = null, + [Description("Rotation around X axis in degrees (Euler)")] float? rotationX = null, + [Description("Rotation around Y axis in degrees (Euler)")] float? rotationY = null, + [Description("Rotation around Z axis in degrees (Euler)")] float? rotationZ = null, + [Description("Scale X factor")] float? scaleX = null, + [Description("Scale Y factor")] float? scaleY = null, + [Description("Scale Z factor")] float? scaleZ = null, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(sceneId, out var assetId)) + { + return new { error = "Invalid scene ID format. Expected a GUID.", transform = (object?)null }; + } + + if (!Guid.TryParse(entityId, out var entityGuid)) + { + return new { error = "Invalid entity ID format. Expected a GUID.", transform = (object?)null }; + } + + var assetVm = session.GetAssetById(assetId); + if (assetVm is not SceneViewModel sceneVm) + { + return new { error = $"Scene not found: {sceneId}", transform = (object?)null }; + } + + // Get the editor for this scene + var editorsManager = session.ServiceProvider.Get(); + if (!editorsManager.TryGetAssetEditor(sceneVm, out var editor)) + { + return new { error = $"Scene is not open in the editor. Use open_scene first: {sceneId}", transform = (object?)null }; + } + + // Find the entity + var absoluteId = new AbsoluteId(assetId, entityGuid); + var partVm = editor.FindPartViewModel(absoluteId); + if (partVm is not EntityViewModel entityVm) + { + return new { error = $"Entity not found in scene: {entityId}", transform = (object?)null }; + } + + var entity = entityVm.AssetSideEntity; + var transformNode = session.AssetNodeContainer.GetOrCreateNode(entity.Transform); + if (transformNode == null) + { + return new { error = "Failed to access transform node for entity.", transform = (object?)null }; + } + + // Read current values + var currentPosition = (Vector3)transformNode[nameof(TransformComponent.Position)].Retrieve(); + var currentRotation = (Quaternion)transformNode[nameof(TransformComponent.Rotation)].Retrieve(); + var currentScale = (Vector3)transformNode[nameof(TransformComponent.Scale)].Retrieve(); + + // Get current Euler angles (radians) via TransformComponent's built-in conversion + var currentEulerRad = entity.Transform.RotationEulerXYZ; + var currentEulerDeg = new Vector3( + MathUtil.RadiansToDegrees(currentEulerRad.X), + MathUtil.RadiansToDegrees(currentEulerRad.Y), + MathUtil.RadiansToDegrees(currentEulerRad.Z)); + + // Build new values, keeping current where not specified + var newPosition = new Vector3( + positionX ?? currentPosition.X, + positionY ?? currentPosition.Y, + positionZ ?? currentPosition.Z); + + var newEulerDeg = new Vector3( + rotationX ?? currentEulerDeg.X, + rotationY ?? currentEulerDeg.Y, + rotationZ ?? currentEulerDeg.Z); + // Convert Euler XYZ degrees → Quaternion (same formula as TransformComponent.RotationEulerXYZ setter) + var newEulerRad = new Vector3( + MathUtil.DegreesToRadians(newEulerDeg.X), + MathUtil.DegreesToRadians(newEulerDeg.Y), + MathUtil.DegreesToRadians(newEulerDeg.Z)); + var newRotation = Quaternion.RotationX(newEulerRad.X) + * Quaternion.RotationY(newEulerRad.Y) + * Quaternion.RotationZ(newEulerRad.Z); + + var newScale = new Vector3( + scaleX ?? currentScale.X, + scaleY ?? currentScale.Y, + scaleZ ?? currentScale.Z); + + // Apply changes through the property graph with undo/redo + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + if (currentPosition != newPosition) + transformNode[nameof(TransformComponent.Position)].Update(newPosition); + if (currentRotation != newRotation) + transformNode[nameof(TransformComponent.Rotation)].Update(newRotation); + if (currentScale != newScale) + transformNode[nameof(TransformComponent.Scale)].Update(newScale); + + undoRedoService.SetName(transaction, $"Set transform '{entityVm.Name}'"); + } + + return new + { + error = (string?)null, + transform = (object)new + { + id = entityId, + name = entityVm.Name, + position = new { x = newPosition.X, y = newPosition.Y, z = newPosition.Z }, + rotation = new { x = newEulerDeg.X, y = newEulerDeg.Y, z = newEulerDeg.Z }, + scale = new { x = newScale.X, y = newScale.Y, z = newScale.Z }, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio.Mcp/Tools/SetUIElementPropertyTool.cs b/sources/editor/Stride.GameStudio.Mcp/Tools/SetUIElementPropertyTool.cs new file mode 100644 index 0000000000..402aebaabd --- /dev/null +++ b/sources/editor/Stride.GameStudio.Mcp/Tools/SetUIElementPropertyTool.cs @@ -0,0 +1,241 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Assets.UI; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.ViewModel; +using Stride.Core.Presentation.Services; +using Stride.Core.Quantum; + +namespace Stride.GameStudio.Mcp.Tools; + +[McpServerToolType] +public sealed class SetUIElementPropertyTool +{ + [McpServerTool(Name = "set_ui_element_property"), Description("Sets a property on a UI element within a UIPageAsset via the property graph. Use get_ui_element to discover available property names. Supports dot-notation paths (e.g. 'Width', 'Text', 'Orientation', 'Margin.Left'). Supports undo/redo.")] + public static async Task SetUIElementProperty( + SessionViewModel session, + DispatcherBridge dispatcher, + [Description("The asset ID of the UIPageAsset")] string assetId, + [Description("The UI element ID (GUID from get_ui_tree)")] string elementId, + [Description("Dot-notation property path relative to the UIElement (e.g. 'Width', 'Text', 'Orientation', 'Margin.Left')")] string propertyPath, + [Description("JSON value to set. Scalar: '200', 'true', '\"Hello\"'. Enum: '\"Horizontal\"'. Color: '{\"r\":1,\"g\":0,\"b\":0,\"a\":1}'. Asset ref: '{\"assetId\":\"GUID\"}'. Clear: 'null'.")] string value, + CancellationToken cancellationToken = default) + { + var result = await dispatcher.InvokeOnUIThread(() => + { + if (!AssetId.TryParse(assetId, out var id)) + { + return new { error = "Invalid asset ID format. Expected a GUID.", result = (object?)null }; + } + + if (!Guid.TryParse(elementId, out var elementGuid)) + { + return new { error = "Invalid element ID format. Expected a GUID.", result = (object?)null }; + } + + var assetVm = session.GetAssetById(id); + if (assetVm == null) + { + return new { error = $"Asset not found: {assetId}", result = (object?)null }; + } + + if (assetVm.Asset is not UIAssetBase uiAsset) + { + return new { error = $"Asset is not a UI page: {assetVm.Name} ({assetVm.AssetType.Name})", result = (object?)null }; + } + + if (!uiAsset.Hierarchy.Parts.TryGetValue(elementGuid, out var design)) + { + return new { error = $"UI element not found: {elementId}", result = (object?)null }; + } + + var rootNode = assetVm.PropertyGraph?.RootNode; + if (rootNode == null) + { + return new { error = "Cannot access property graph for this asset.", result = (object?)null }; + } + + // Parse the JSON value + JsonElement jsonValue; + try + { + jsonValue = JsonSerializer.Deserialize(value); + } + catch (JsonException ex) + { + return new { error = $"Invalid JSON value: {ex.Message}", result = (object?)null }; + } + + // Navigate to the element's node in the property graph: + // RootNode -> Hierarchy -> Parts -> [elementGuid] -> UIElement + var hierarchyMember = rootNode.TryGetChild(nameof(UIAssetBase.Hierarchy)); + if (hierarchyMember?.Target == null) + { + return new { error = "Cannot navigate to Hierarchy node.", result = (object?)null }; + } + + var partsMember = hierarchyMember.Target.TryGetChild("Parts"); + if (partsMember?.Target == null) + { + return new { error = "Cannot navigate to Parts node.", result = (object?)null }; + } + + // Find the element's design node in the parts dictionary + IObjectNode? designNode = null; + foreach (var idx in partsMember.Target.Indices) + { + var candidate = partsMember.Target.IndexedTarget(idx); + if (candidate != null) + { + var uiElementMember = candidate.TryGetChild(nameof(UIElementDesign.UIElement)); + if (uiElementMember?.Target != null) + { + var idMember = uiElementMember.Target.TryGetChild("Id"); + if (idMember != null && idMember.Retrieve() is Guid partId && partId == elementGuid) + { + designNode = candidate; + break; + } + } + } + } + + if (designNode == null) + { + return new { error = $"Cannot find UI element node in property graph: {elementId}", result = (object?)null }; + } + + var uiElementNode = designNode.TryGetChild(nameof(UIElementDesign.UIElement)); + if (uiElementNode?.Target == null) + { + return new { error = "Cannot navigate to UIElement node in property graph.", result = (object?)null }; + } + + // Navigate the property path from the UIElement node + var segments = propertyPath.Split('.'); + IObjectNode currentObject = uiElementNode.Target; + IMemberNode? leafMember = null; + + for (int i = 0; i < segments.Length; i++) + { + var segment = segments[i]; + + // Handle indexed access: "Layers[0]" + string memberName = segment; + int? segIndex = null; + var bracketStart = segment.IndexOf('['); + if (bracketStart >= 0) + { + memberName = segment[..bracketStart]; + var bracketEnd = segment.IndexOf(']'); + if (bracketEnd > bracketStart + 1 && + int.TryParse(segment[(bracketStart + 1)..bracketEnd], out var parsedIndex)) + { + segIndex = parsedIndex; + } + } + + var member = currentObject.TryGetChild(memberName); + if (member == null) + { + var availableMembers = currentObject.Members.Select(m => m.Name).OrderBy(n => n).ToArray(); + return new + { + error = $"Property '{memberName}' not found at path level {i}. Available properties: {string.Join(", ", availableMembers)}", + result = (object?)null, + }; + } + + if (i == segments.Length - 1 && segIndex == null) + { + leafMember = member; + } + else + { + var target = member.Target; + if (segIndex.HasValue && target != null) + { + try + { + target = target.IndexedTarget(new NodeIndex(segIndex.Value)); + } + catch + { + return new + { + error = $"Index [{segIndex.Value}] is out of range for property '{memberName}'.", + result = (object?)null, + }; + } + } + + if (target == null) + { + return new + { + error = $"Cannot navigate into property '{memberName}' — it has no target object (value may be null).", + result = (object?)null, + }; + } + + currentObject = target; + + if (i == segments.Length - 1 && segIndex.HasValue) + { + return new + { + error = $"Path ends with an indexed access '{segment}'. Add a property name after the index (e.g. '{segment}.PropertyName').", + result = (object?)null, + }; + } + } + } + + if (leafMember == null) + { + return new { error = "Could not resolve property path.", result = (object?)null }; + } + + // Convert the value + object? convertedValue; + try + { + convertedValue = JsonTypeConverter.ConvertJsonToType(jsonValue, leafMember.Type, session); + } + catch (Exception ex) + { + return new { error = $"Cannot convert value to type {leafMember.Type.Name}: {ex.Message}", result = (object?)null }; + } + + // Apply in undo/redo transaction + var undoRedoService = session.ServiceProvider.Get(); + using (var transaction = undoRedoService.CreateTransaction()) + { + leafMember.Update(convertedValue); + undoRedoService.SetName(transaction, $"Set {propertyPath} on UI element '{design.UIElement.Name ?? design.UIElement.GetType().Name}'"); + } + + return new + { + error = (string?)null, + result = (object)new + { + assetId = assetVm.Id.ToString(), + elementId = elementId, + propertyPath, + newValueType = leafMember.Type.Name, + }, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } +} diff --git a/sources/editor/Stride.GameStudio/Program.cs b/sources/editor/Stride.GameStudio/Program.cs index 7bccfc7dbd..c7e6266d44 100644 --- a/sources/editor/Stride.GameStudio/Program.cs +++ b/sources/editor/Stride.GameStudio/Program.cs @@ -36,6 +36,7 @@ using Stride.Editor.Build; using Stride.Editor.Preview; using Stride.GameStudio.Helpers; +using Stride.GameStudio.Mcp; using Stride.GameStudio.Plugin; using Stride.GameStudio.Services; using Stride.GameStudio.View; @@ -252,6 +253,7 @@ private static async void Startup(UFile initialSessionPath) AssetsPlugin.RegisterPlugin(typeof(StrideDefaultAssetsPlugin)); var strideEditorPlugin = (StrideEditorPlugin)AssetsPlugin.RegisterPlugin(typeof(StrideEditorPlugin)); strideEditorPlugin.EnableThumbnailService = enableThumbnailServices; + AssetsPlugin.RegisterPlugin(typeof(McpEditorPlugin)); // Attempt to load the startup session, if available if (!UPath.IsNullOrEmpty(initialSessionPath)) diff --git a/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj b/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj index 03a26a86cf..8cadb61254 100644 --- a/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj +++ b/sources/editor/Stride.GameStudio/Stride.GameStudio.csproj @@ -72,6 +72,7 @@ false + diff --git a/sources/engine/Stride.Engine.Mcp.Tests/GameFixture.cs b/sources/engine/Stride.Engine.Mcp.Tests/GameFixture.cs new file mode 100644 index 0000000000..1aebb87876 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/GameFixture.cs @@ -0,0 +1,216 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics; +using System.Text; +using Xunit; + +namespace Stride.Engine.Mcp.Tests; + +/// +/// xUnit collection fixture that manages a game process lifecycle for MCP integration tests. +/// Launches a test game with MCP enabled, waits for the SSE endpoint to become ready, +/// and kills the process after all tests in the collection complete. +/// +public sealed class GameFixture : IAsyncLifetime +{ + private const string RepoRootMarker = @"build\Stride.sln"; + + private static readonly TimeSpan StartupTimeout = TimeSpan.FromSeconds(60); + private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2); + private static readonly TimeSpan ShutdownTimeout = TimeSpan.FromSeconds(10); + + private Process? _process; + private HttpClient? _httpClient; + private readonly StringBuilder _stdout = new(); + private readonly StringBuilder _stderr = new(); + + /// + /// The port the MCP server is running on. + /// + public int Port { get; private set; } = 5272; + + /// + /// Whether the fixture successfully started the game and the MCP server is ready. + /// + public bool IsReady { get; private set; } + + private static bool IsEnabled => + string.Equals( + Environment.GetEnvironmentVariable("STRIDE_MCP_GAME_INTEGRATION_TESTS"), + "true", + StringComparison.OrdinalIgnoreCase); + + public async Task InitializeAsync() + { + if (!IsEnabled) + return; + + // Read port override + var portStr = Environment.GetEnvironmentVariable("STRIDE_MCP_GAME_PORT"); + if (int.TryParse(portStr, out var port)) + Port = port; + + // Resolve game executable path + var exePath = ResolveGameExePath(); + + if (string.IsNullOrEmpty(exePath) || !File.Exists(exePath)) + { + throw new InvalidOperationException( + $"Game executable not found at: {exePath}\n\n" + + "Set STRIDE_MCP_GAME_EXE to the path of a built game with Stride.Engine.Mcp enabled."); + } + + // Launch the game with MCP enabled + var startInfo = new ProcessStartInfo + { + FileName = exePath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = false, + }; + startInfo.Environment["STRIDE_MCP_GAME_ENABLED"] = "true"; + startInfo.Environment["STRIDE_MCP_GAME_PORT"] = Port.ToString(); + + _process = Process.Start(startInfo); + if (_process == null) + { + throw new InvalidOperationException("Failed to start game process."); + } + + _process.OutputDataReceived += (_, args) => + { + if (args.Data != null) + lock (_stdout) { _stdout.AppendLine(args.Data); } + }; + _process.ErrorDataReceived += (_, args) => + { + if (args.Data != null) + lock (_stderr) { _stderr.AppendLine(args.Data); } + }; + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + + // Wait for MCP server to become ready + _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + var endpoint = $"http://localhost:{Port}/sse"; + var deadline = DateTime.UtcNow + StartupTimeout; + + while (DateTime.UtcNow < deadline) + { + if (_process.HasExited) + { + var output = GetCapturedOutput(); + throw new InvalidOperationException( + $"Game exited prematurely with code {_process.ExitCode}.\n\n" + + $"Captured output:\n{output}"); + } + + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var response = await _httpClient.GetAsync(endpoint, HttpCompletionOption.ResponseHeadersRead, cts.Token); + if (response.IsSuccessStatusCode) + { + IsReady = true; + return; + } + } + catch (Exception) when (!_process.HasExited) + { + // Server not ready yet + } + + await Task.Delay(PollInterval); + } + + var timeoutOutput = GetCapturedOutput(); + await KillProcessAsync(); + throw new TimeoutException( + $"Game MCP server did not become ready within {StartupTimeout.TotalSeconds}s.\n" + + $"Endpoint: {endpoint}\n\n" + + $"Captured output:\n{timeoutOutput}"); + } + + public async Task DisposeAsync() + { + await KillProcessAsync(); + _httpClient?.Dispose(); + } + + private async Task KillProcessAsync() + { + if (_process == null || _process.HasExited) + { + _process?.Dispose(); + _process = null; + return; + } + + try + { + _process.Kill(entireProcessTree: true); + using var cts = new CancellationTokenSource(ShutdownTimeout); + await _process.WaitForExitAsync(cts.Token); + } + catch (Exception) + { + // Best effort + } + finally + { + _process.Dispose(); + _process = null; + } + } + + private static string? ResolveGameExePath() + { + // Explicit override takes priority + var envPath = Environment.GetEnvironmentVariable("STRIDE_MCP_GAME_EXE"); + if (!string.IsNullOrEmpty(envPath)) + return envPath; + + // Auto-discover: walk up from the test assembly to find the repo root, + // then resolve the TestGame exe at its known build output path. + var dir = AppContext.BaseDirectory; + while (dir != null) + { + if (File.Exists(Path.Combine(dir, RepoRootMarker))) + { + var candidate = Path.Combine(dir, + "sources", "engine", "Stride.Engine.Mcp.Tests", "TestGame", + "bin", "Debug", "net10.0", "Direct3D11", "TestGame.exe"); + if (File.Exists(candidate)) + return candidate; + break; + } + dir = Path.GetDirectoryName(dir); + } + + return null; + } + + private string GetCapturedOutput() + { + var sb = new StringBuilder(); + lock (_stdout) + { + if (_stdout.Length > 0) + { + sb.AppendLine("--- stdout ---"); + sb.Append(_stdout); + } + } + lock (_stderr) + { + if (_stderr.Length > 0) + { + sb.AppendLine("--- stderr ---"); + sb.Append(_stderr); + } + } + return sb.Length > 0 ? sb.ToString() : "(no output captured)"; + } +} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationCollection.cs b/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationCollection.cs new file mode 100644 index 0000000000..c898053a26 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationCollection.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Xunit; + +namespace Stride.Engine.Mcp.Tests; + +/// +/// Defines the xUnit test collection for game MCP integration tests. +/// All test classes using [Collection("GameMcpIntegration")] share a single +/// instance (one game process for all tests). +/// +[CollectionDefinition("GameMcpIntegration")] +public class GameMcpIntegrationCollection : ICollectionFixture +{ +} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationFactAttribute.cs b/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationFactAttribute.cs new file mode 100644 index 0000000000..6ddaa4fa76 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationFactAttribute.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Xunit; + +namespace Stride.Engine.Mcp.Tests; + +/// +/// A custom Fact attribute that skips the test unless the STRIDE_MCP_GAME_INTEGRATION_TESTS +/// environment variable is set to "true". This ensures integration tests that require +/// a running game instance with MCP enabled are disabled by default. +/// +public sealed class GameMcpIntegrationFactAttribute : FactAttribute +{ + private const string EnvVar = "STRIDE_MCP_GAME_INTEGRATION_TESTS"; + + public GameMcpIntegrationFactAttribute() + { + if (!string.Equals(Environment.GetEnvironmentVariable(EnvVar), "true", StringComparison.OrdinalIgnoreCase)) + { + Skip = $"Game MCP integration tests are disabled. Set {EnvVar}=true and ensure a game is running with MCP enabled."; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationTests.cs b/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationTests.cs new file mode 100644 index 0000000000..03687b2ddd --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/GameMcpIntegrationTests.cs @@ -0,0 +1,651 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Text.Json; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using Xunit; + +namespace Stride.Engine.Mcp.Tests; + +/// +/// Integration tests for the MCP server embedded in a running Stride game. +/// The automatically launches a test game and waits +/// for the MCP server to become ready. Tests are skipped unless the environment +/// variable STRIDE_MCP_GAME_INTEGRATION_TESTS is set to "true". +/// +[Collection("GameMcpIntegration")] +public sealed class GameMcpIntegrationTests : IAsyncLifetime +{ + private readonly GameFixture _fixture; + private McpClient? _client; + + public GameMcpIntegrationTests(GameFixture fixture) + { + _fixture = fixture; + } + + public async Task InitializeAsync() + { + if (!_fixture.IsReady) + return; + + var transport = new HttpClientTransport(new HttpClientTransportOptions + { + Endpoint = new Uri($"http://localhost:{_fixture.Port}/sse"), + Name = "Stride Game MCP Integration Tests", + }); + + _client = await McpClient.CreateAsync(transport); + } + + public async Task DisposeAsync() + { + if (_client != null) + { + await ((IAsyncDisposable)_client).DisposeAsync(); + } + } + + private async Task CallToolAsync(string toolName, Dictionary? args = null) + { + Assert.NotNull(_client); + + var response = await _client!.CallToolAsync(toolName, args); + + var textContent = response.Content.OfType().FirstOrDefault(); + Assert.NotNull(textContent); + return JsonDocument.Parse(textContent!.Text!).RootElement; + } + + // ==================== Tool Discovery ==================== + + [GameMcpIntegrationFact] + public async Task ListTools_ReturnsAllExpectedTools() + { + Assert.NotNull(_client); + var tools = await _client!.ListToolsAsync(); + var toolNames = tools.Select(t => t.Name).ToList(); + + Assert.Contains("get_game_status", toolNames); + Assert.Contains("get_scene_entities", toolNames); + Assert.Contains("get_entity", toolNames); + Assert.Contains("capture_screenshot", toolNames); + Assert.Contains("get_logs", toolNames); + Assert.Contains("simulate_input", toolNames); + Assert.Contains("focus_element", toolNames); + Assert.Contains("describe_viewport", toolNames); + } + + // ==================== Status & Inspection ==================== + + [GameMcpIntegrationFact] + public async Task GetGameStatus_ReturnsRunningGame() + { + var result = await CallToolAsync("get_game_status"); + + Assert.Equal("running", result.GetProperty("status").GetString()); + Assert.True(result.GetProperty("fps").GetDouble() >= 0, "FPS should be non-negative"); + Assert.True(result.GetProperty("entityCount").GetInt32() > 0); + Assert.False(string.IsNullOrEmpty(result.GetProperty("activeSceneUrl").GetString())); + } + + [GameMcpIntegrationFact] + public async Task GetSceneEntities_ReturnsEntityTree() + { + var result = await CallToolAsync("get_scene_entities"); + + var entities = result.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0); + + var firstEntity = entities[0]; + Assert.True(firstEntity.TryGetProperty("id", out _)); + Assert.True(firstEntity.TryGetProperty("name", out _)); + Assert.True(firstEntity.TryGetProperty("children", out _) || firstEntity.TryGetProperty("childCount", out _)); + } + + [GameMcpIntegrationFact] + public async Task GetSceneEntities_WithMaxDepth() + { + var result = await CallToolAsync("get_scene_entities", new Dictionary + { + ["maxDepth"] = 1, + }); + + var entities = result.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0); + + // At depth 1, children should exist but their children should not go deeper + var firstEntity = entities[0]; + if (firstEntity.TryGetProperty("children", out var children) && children.GetArrayLength() > 0) + { + var child = children[0]; + // At maxDepth=1, the children at depth 1 should not have their own children array + // (they're at the limit) + if (child.TryGetProperty("children", out var grandchildren)) + { + // Grandchildren are at depth 2 which exceeds maxDepth 1 — so this should not have further expansion + foreach (var gc in grandchildren.EnumerateArray()) + { + Assert.False(gc.TryGetProperty("children", out _), + "Hierarchy should be truncated at maxDepth"); + } + } + } + } + + [GameMcpIntegrationFact] + public async Task GetEntity_ByName_ReturnsComponents() + { + // First get the entity list to find a known entity name + var sceneResult = await CallToolAsync("get_scene_entities"); + var entities = sceneResult.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0); + + var entityName = entities[0].GetProperty("name").GetString()!; + + var result = await CallToolAsync("get_entity", new Dictionary + { + ["entityName"] = entityName, + }); + + Assert.True(result.TryGetProperty("components", out var components)); + Assert.True(components.GetArrayLength() > 0); + + // Should have at least TransformComponent + var hasTransform = false; + foreach (var comp in components.EnumerateArray()) + { + if (comp.GetProperty("type").GetString() == "TransformComponent") + { + hasTransform = true; + break; + } + } + Assert.True(hasTransform, "Entity should have a TransformComponent"); + } + + [GameMcpIntegrationFact] + public async Task GetEntity_ByInvalidId_ReturnsError() + { + var result = await CallToolAsync("get_entity", new Dictionary + { + ["entityId"] = "00000000-0000-0000-0000-000000000000", + }); + + Assert.True(result.TryGetProperty("error", out _)); + } + + [GameMcpIntegrationFact] + public async Task GetEntity_ComponentSerialization() + { + // Get any entity with a TransformComponent + var sceneResult = await CallToolAsync("get_scene_entities"); + var entities = sceneResult.GetProperty("entities"); + var entityName = entities[0].GetProperty("name").GetString()!; + + var result = await CallToolAsync("get_entity", new Dictionary + { + ["entityName"] = entityName, + }); + + var components = result.GetProperty("components"); + foreach (var comp in components.EnumerateArray()) + { + if (comp.GetProperty("type").GetString() == "TransformComponent") + { + var props = comp.GetProperty("properties"); + // TransformComponent should have Position with x/y/z + Assert.True(props.TryGetProperty("Position", out var position)); + Assert.True(position.TryGetProperty("x", out _)); + Assert.True(position.TryGetProperty("y", out _)); + Assert.True(position.TryGetProperty("z", out _)); + return; + } + } + + Assert.Fail("No TransformComponent found to verify serialization"); + } + + // ==================== Screenshot ==================== + + [GameMcpIntegrationFact] + public async Task CaptureScreenshot_ReturnsBase64Png() + { + Assert.NotNull(_client); + var response = await _client!.CallToolAsync("capture_screenshot"); + + var imageContent = response.Content.OfType().FirstOrDefault(); + Assert.NotNull(imageContent); + Assert.Equal("image/png", imageContent!.MimeType); + + // Decode and verify PNG header + var bytes = Convert.FromBase64String(imageContent.Data!); + Assert.True(bytes.Length > 8, "Image should have content"); + // PNG magic bytes: 0x89 0x50 0x4E 0x47 + Assert.Equal(0x89, bytes[0]); + Assert.Equal(0x50, bytes[1]); + Assert.Equal(0x4E, bytes[2]); + Assert.Equal(0x47, bytes[3]); + } + + // ==================== Logs ==================== + + [GameMcpIntegrationFact] + public async Task GetLogs_ReturnsEntries() + { + var result = await CallToolAsync("get_logs"); + + Assert.True(result.ValueKind == JsonValueKind.Array); + if (result.GetArrayLength() > 0) + { + var entry = result[0]; + Assert.True(entry.TryGetProperty("timestamp", out _)); + Assert.True(entry.TryGetProperty("module", out _)); + Assert.True(entry.TryGetProperty("level", out _)); + Assert.True(entry.TryGetProperty("message", out _)); + } + } + + [GameMcpIntegrationFact] + public async Task GetLogs_WithMinLevel() + { + var result = await CallToolAsync("get_logs", new Dictionary + { + ["minLevel"] = "Warning", + }); + + Assert.True(result.ValueKind == JsonValueKind.Array); + foreach (var entry in result.EnumerateArray()) + { + var level = entry.GetProperty("level").GetString(); + Assert.True(level == "Warning" || level == "Error" || level == "Fatal", + $"Expected Warning or higher, got {level}"); + } + } + + [GameMcpIntegrationFact] + public async Task GetLogs_WithMaxCount() + { + var result = await CallToolAsync("get_logs", new Dictionary + { + ["maxCount"] = 5, + }); + + Assert.True(result.ValueKind == JsonValueKind.Array); + Assert.True(result.GetArrayLength() <= 5); + } + + // ==================== Input Injection ==================== + + [GameMcpIntegrationFact] + public async Task SimulateInput_KeyPress() + { + var result = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "key_press", + ["key"] = "Space", + }); + + Assert.True(result.GetProperty("success").GetBoolean()); + } + + [GameMcpIntegrationFact] + public async Task SimulateInput_MouseMove() + { + var result = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "mouse_move", + ["position"] = "0.5,0.5", + }); + + Assert.True(result.GetProperty("success").GetBoolean()); + } + + [GameMcpIntegrationFact] + public async Task SimulateInput_MouseClick() + { + var result = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "mouse_click", + ["button"] = "Left", + }); + + Assert.True(result.GetProperty("success").GetBoolean()); + } + + [GameMcpIntegrationFact] + public async Task SimulateInput_GamepadButton() + { + var result = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "gamepad_button_press", + ["gamepadButton"] = "A", + }); + + Assert.True(result.GetProperty("success").GetBoolean()); + } + + [GameMcpIntegrationFact] + public async Task SimulateInput_GamepadAxis() + { + var result = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "gamepad_axis", + ["gamepadAxis"] = "LeftThumbX", + ["axisValue"] = 0.75, + }); + + Assert.True(result.GetProperty("success").GetBoolean()); + } + + [GameMcpIntegrationFact] + public async Task SimulateInput_InvalidKey_ReturnsError() + { + var result = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "key_press", + ["key"] = "NotAKey", + }); + + Assert.True(result.TryGetProperty("error", out _)); + } + + // ==================== Focus Element ==================== + + [GameMcpIntegrationFact] + public async Task FocusElement_Entity_ReturnsScreenPosition() + { + // Get a known entity name + var sceneResult = await CallToolAsync("get_scene_entities"); + var entities = sceneResult.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0); + var entityName = entities[0].GetProperty("name").GetString()!; + + var result = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "entity", + ["entityName"] = entityName, + }); + + // Entity might be off-screen, so check for either success or expected error + if (result.TryGetProperty("success", out var success) && success.GetBoolean()) + { + var nx = result.GetProperty("normalizedX").GetDouble(); + var ny = result.GetProperty("normalizedY").GetDouble(); + Assert.InRange(nx, 0.0, 1.0); + Assert.InRange(ny, 0.0, 1.0); + } + else + { + // Acceptable if entity is off-screen + Assert.True(result.TryGetProperty("error", out _)); + } + } + + [GameMcpIntegrationFact] + public async Task FocusElement_InvalidEntity_ReturnsError() + { + var result = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "entity", + ["entityName"] = "NonExistentEntity_12345", + }); + + Assert.True(result.TryGetProperty("error", out _)); + } + + [GameMcpIntegrationFact] + public async Task FocusElement_Entity_ThenMouseClick() + { + // Get a known entity name + var sceneResult = await CallToolAsync("get_scene_entities"); + var entities = sceneResult.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0); + var entityName = entities[0].GetProperty("name").GetString()!; + + // Focus on entity + var focusResult = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "entity", + ["entityName"] = entityName, + }); + + // Mouse click (should succeed regardless of focus result) + var clickResult = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "mouse_click", + ["button"] = "Left", + }); + + Assert.True(clickResult.GetProperty("success").GetBoolean()); + } + + // ==================== Focus UI Element ==================== + + /// + /// Helper to focus a UI element and assert its normalized position is near the expected value. + /// The TestGame has a Grid root filling the 800x600 virtual resolution with three named elements: + /// - TopLeftButton: 100x40, Left/Top aligned, Margin(Left:50, Top:30) → center at virtual (100, 50) → normalized (0.125, 0.083) + /// - CenterButton: 200x50, Center/Center aligned → center at virtual (400, 300) → normalized (0.5, 0.5) + /// - BottomRightButton: 150x60, Right/Bottom aligned, Margin(Right:50, Bottom:30) → center at virtual (675, 540) → normalized (0.84375, 0.9) + /// + private static void AssertNormalizedPosition(JsonElement result, double expectedNX, double expectedNY, double tolerance = 0.03) + { + Assert.True(result.TryGetProperty("success", out var success), $"Expected success but got: {result}"); + Assert.True(success.GetBoolean()); + + var nx = result.GetProperty("normalizedX").GetDouble(); + var ny = result.GetProperty("normalizedY").GetDouble(); + + Assert.True(Math.Abs(nx - expectedNX) < tolerance, + $"normalizedX: expected ~{expectedNX:F3} but got {nx:F3} (diff={Math.Abs(nx - expectedNX):F4}). Full response: {result}"); + Assert.True(Math.Abs(ny - expectedNY) < tolerance, + $"normalizedY: expected ~{expectedNY:F3} but got {ny:F3} (diff={Math.Abs(ny - expectedNY):F4}). Full response: {result}"); + } + + [GameMcpIntegrationFact] + public async Task FocusElement_UI_TopLeftButton() + { + // TopLeftButton: 100x40, HorizontalAlignment=Left, VerticalAlignment=Top, Margin(Left:50, Top:30) + // Center at virtual (100, 50) in 800x600 → normalized (0.125, 0.0833) + var result = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "TopLeftButton", + }); + + AssertNormalizedPosition(result, 0.125, 0.0833); + } + + [GameMcpIntegrationFact] + public async Task FocusElement_UI_CenterButton() + { + // CenterButton: 200x50, HorizontalAlignment=Center, VerticalAlignment=Center + // Center at virtual (400, 300) in 800x600 → normalized (0.5, 0.5) + var result = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "CenterButton", + }); + + AssertNormalizedPosition(result, 0.5, 0.5); + } + + [GameMcpIntegrationFact] + public async Task FocusElement_UI_BottomRightButton() + { + // BottomRightButton: 150x60, HorizontalAlignment=Right, VerticalAlignment=Bottom, Margin(Right:50, Bottom:30) + // Center at virtual (675, 540) in 800x600 → normalized (0.84375, 0.9) + var result = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "BottomRightButton", + }); + + AssertNormalizedPosition(result, 0.84375, 0.9); + } + + [GameMcpIntegrationFact] + public async Task FocusElement_UI_PositionsAreDifferent() + { + // Verify all three buttons return distinct positions (guards against all returning 0,0) + var topLeft = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "TopLeftButton", + }); + var center = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "CenterButton", + }); + var bottomRight = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "BottomRightButton", + }); + + Assert.True(topLeft.GetProperty("success").GetBoolean()); + Assert.True(center.GetProperty("success").GetBoolean()); + Assert.True(bottomRight.GetProperty("success").GetBoolean()); + + var tlX = topLeft.GetProperty("normalizedX").GetDouble(); + var cX = center.GetProperty("normalizedX").GetDouble(); + var brX = bottomRight.GetProperty("normalizedX").GetDouble(); + + // TopLeft.X < Center.X < BottomRight.X + Assert.True(tlX < cX, $"TopLeft.X ({tlX:F3}) should be less than Center.X ({cX:F3})"); + Assert.True(cX < brX, $"Center.X ({cX:F3}) should be less than BottomRight.X ({brX:F3})"); + + var tlY = topLeft.GetProperty("normalizedY").GetDouble(); + var cY = center.GetProperty("normalizedY").GetDouble(); + var brY = bottomRight.GetProperty("normalizedY").GetDouble(); + + // TopLeft.Y < Center.Y < BottomRight.Y + Assert.True(tlY < cY, $"TopLeft.Y ({tlY:F3}) should be less than Center.Y ({cY:F3})"); + Assert.True(cY < brY, $"Center.Y ({cY:F3}) should be less than BottomRight.Y ({brY:F3})"); + } + + [GameMcpIntegrationFact] + public async Task FocusElement_InvalidUIElement_ReturnsError() + { + var result = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "NonExistentElement_12345", + }); + + Assert.True(result.TryGetProperty("error", out _)); + } + + [GameMcpIntegrationFact] + public async Task FocusElement_UI_ThenMouseClick() + { + // Focus on the top-left button, then click + var focusResult = await CallToolAsync("focus_element", new Dictionary + { + ["target"] = "ui", + ["elementName"] = "TopLeftButton", + }); + + Assert.True(focusResult.TryGetProperty("success", out var success), $"Expected success but got: {focusResult}"); + Assert.True(success.GetBoolean()); + + // Mouse click at the focused position (should succeed) + var clickResult = await CallToolAsync("simulate_input", new Dictionary + { + ["action"] = "mouse_click", + ["button"] = "Left", + }); + + Assert.True(clickResult.GetProperty("success").GetBoolean()); + } + + // ==================== Describe Viewport ==================== + + [GameMcpIntegrationFact] + public async Task DescribeViewport_ReturnsEntities() + { + var result = await CallToolAsync("describe_viewport"); + + Assert.False(result.TryGetProperty("error", out var err) && err.ValueKind == JsonValueKind.String && err.GetString() != null, + $"Unexpected error: {result}"); + + // Camera info should be present + var camera = result.GetProperty("camera"); + Assert.True(camera.TryGetProperty("position", out _)); + Assert.True(camera.TryGetProperty("projection", out _)); + Assert.True(camera.TryGetProperty("fieldOfViewDegrees", out _)); + + // Should have entities + var totalEntities = result.GetProperty("totalEntitiesInScene").GetInt32(); + Assert.True(totalEntities > 0, "Expected at least one entity in the scene"); + + var entities = result.GetProperty("entities"); + Assert.True(entities.GetArrayLength() > 0, "Expected at least one entity in describe_viewport result"); + + // Each entity should have required fields + var first = entities[0]; + Assert.True(first.TryGetProperty("id", out _)); + Assert.True(first.TryGetProperty("name", out _)); + Assert.True(first.TryGetProperty("worldPosition", out _)); + Assert.True(first.TryGetProperty("distanceToCamera", out _)); + Assert.True(first.TryGetProperty("isVisible", out _)); + Assert.True(first.TryGetProperty("components", out _)); + } + + [GameMcpIntegrationFact] + public async Task DescribeViewport_VisibleEntitiesHaveScreenPosition() + { + var result = await CallToolAsync("describe_viewport"); + + var entities = result.GetProperty("entities"); + bool foundVisible = false; + + for (int i = 0; i < entities.GetArrayLength(); i++) + { + var entity = entities[i]; + if (entity.GetProperty("isVisible").GetBoolean()) + { + foundVisible = true; + Assert.True(entity.TryGetProperty("screenPosition", out var screenPos)); + Assert.NotEqual(JsonValueKind.Null, screenPos.ValueKind); + var sx = screenPos.GetProperty("x").GetDouble(); + var sy = screenPos.GetProperty("y").GetDouble(); + Assert.InRange(sx, -0.5, 1.5); + Assert.InRange(sy, -0.5, 1.5); + break; + } + } + + Assert.True(foundVisible, "Expected at least one visible entity in the viewport"); + } + + [GameMcpIntegrationFact] + public async Task DescribeViewport_MaxEntities_LimitsResults() + { + var result = await CallToolAsync("describe_viewport", new Dictionary + { + ["maxEntities"] = 2, + }); + + var entities = result.GetProperty("entities"); + Assert.True(entities.GetArrayLength() <= 2, $"Expected at most 2 entities, got {entities.GetArrayLength()}"); + } + + [GameMcpIntegrationFact] + public async Task DescribeViewport_CameraHasReasonableValues() + { + var result = await CallToolAsync("describe_viewport"); + + var camera = result.GetProperty("camera"); + var fov = camera.GetProperty("fieldOfViewDegrees").GetDouble(); + Assert.InRange(fov, 1, 179); // Reasonable FOV range + + var nearPlane = camera.GetProperty("nearPlane").GetDouble(); + var farPlane = camera.GetProperty("farPlane").GetDouble(); + Assert.True(nearPlane > 0, "Near plane should be positive"); + Assert.True(farPlane > nearPlane, "Far plane should be greater than near plane"); + } +} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/Stride.Engine.Mcp.Tests.csproj b/sources/engine/Stride.Engine.Mcp.Tests/Stride.Engine.Mcp.Tests.csproj new file mode 100644 index 0000000000..c9d57cd517 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/Stride.Engine.Mcp.Tests.csproj @@ -0,0 +1,24 @@ + + + net10.0 + win-x64 + enable + enable + true + false + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/CubeModel.sdpromodel b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/CubeModel.sdpromodel new file mode 100644 index 0000000000..4187d1460d --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/CubeModel.sdpromodel @@ -0,0 +1,12 @@ +!ProceduralModelAsset +Id: e4f5a6b7-c8d9-0123-efab-345678901234 +SerializedVersion: {Stride: 2.0.0.0} +Tags: [] +Type: !CubeProceduralModel + Size: {X: 1.0, Y: 1.0, Z: 1.0} + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + UvScale: {X: 1.0, Y: 1.0} + LocalOffset: {X: 0.0, Y: 0.0, Z: 0.0} + NumberOfTextureCoordinates: 10 + MaterialInstance: + Material: null diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GameSettings.sdgamesettings b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GameSettings.sdgamesettings new file mode 100644 index 0000000000..58a576f84a --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GameSettings.sdgamesettings @@ -0,0 +1,26 @@ +!GameSettingsAsset +Id: b1c2d3e4-f5a6-7890-bcde-f12345678901 +SerializedVersion: {Stride: 3.1.0.1} +Tags: [] +DefaultScene: d3e4f5a6-b7c8-9012-defa-234567890123:MainScene +GraphicsCompositor: c2d3e4f5-a6b7-8901-cdef-123456789012:GraphicsCompositor +Defaults: + - !Stride.Graphics.RenderingSettings,Stride.Graphics + DefaultBackBufferWidth: 800 + DefaultBackBufferHeight: 600 + AdaptBackBufferToScreen: false + DefaultGraphicsProfile: Level_9_1 + ColorSpace: Linear + DisplayOrientation: Default + - !Stride.Audio.AudioEngineSettings,Stride.Audio + HrtfSupport: false + - !Stride.Streaming.StreamingSettings,Stride.Rendering + Enabled: false + ManagerUpdatesInterval: 0:00:00:00.0330000 + ResourceLiveTimeout: 0:00:00:08.0000000 + - !Stride.Assets.EditorSettings,Stride.Assets + RenderingMode: HDR +Overrides: [] +PlatformFilters: [] +SplashScreenTexture: null +SplashScreenColor: {R: 0, G: 0, B: 0, A: 255} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GraphicsCompositor.sdgfxcomp b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GraphicsCompositor.sdgfxcomp new file mode 100644 index 0000000000..5990870ab2 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GraphicsCompositor.sdgfxcomp @@ -0,0 +1,84 @@ +!GraphicsCompositorAsset +Id: c2d3e4f5-a6b7-8901-cdef-123456789012 +SerializedVersion: {Stride: 3.1.0.1} +Tags: [] +Cameras: + de2e75c3b2b23e54162686363f3f138e: + Id: 07117963-ef3b-47b6-9344-96aa198edeea + Name: Main +RenderStages: + 47116750c1a5d449b4ad3625f71439b3: + Id: eab72911-5b68-41dd-bfa3-f0ab0e714b4a + Name: Opaque + EffectSlotName: Main + SortMode: !SortModeStateChange {} + 9105a30fee026d4893472b6aee83d035: + Id: 31169f13-1e38-4c68-a1e4-1fd9c6d148c3 + Name: Transparent + EffectSlotName: Main + SortMode: !BackToFrontSortMode {} + 554e52c061404d4684dd7c4c70f70e0e: + Id: 14d1fa9d-90a2-40df-9e6f-bf3fa08f7774 + Name: ShadowMapCaster + EffectSlotName: ShadowMapCaster + SortMode: !FrontToBackSortMode {} +RenderFeatures: + d8fb80b0e7995140a46bca8dc36ee8a2: !Stride.Rendering.MeshRenderFeature,Stride.Rendering + RenderStageSelectors: + 44cf4a95ef82544e9ce3c6507d5569a9: !Stride.Rendering.MeshTransparentRenderStageSelector,Stride.Rendering + OpaqueRenderStage: ref!! eab72911-5b68-41dd-bfa3-f0ab0e714b4a + TransparentRenderStage: ref!! 31169f13-1e38-4c68-a1e4-1fd9c6d148c3 + EffectName: StrideForwardShadingEffect + 6f7224048750e7260ea87c444f74b32c: !Stride.Rendering.Shadows.ShadowMapRenderStageSelector,Stride.Rendering + ShadowMapRenderStage: ref!! 14d1fa9d-90a2-40df-9e6f-bf3fa08f7774 + EffectName: StrideForwardShadingEffect.ShadowMapCaster + PipelineProcessors: + d70f5aee0616e4ab25081ceaf643290c: !Stride.Rendering.MeshPipelineProcessor,Stride.Rendering + TransparentRenderStage: ref!! 31169f13-1e38-4c68-a1e4-1fd9c6d148c3 + 26c899b17f88c21ab13bf60a7220ccd1: !Stride.Rendering.ShadowMeshPipelineProcessor,Stride.Rendering + ShadowMapRenderStage: ref!! 14d1fa9d-90a2-40df-9e6f-bf3fa08f7774 + RenderFeatures: + 86b959cbdf51a1438d4973177c77c627: !Stride.Rendering.TransformRenderFeature,Stride.Rendering {} + 8e0351fee9883922648a11016224b195: !Stride.Rendering.SkinningRenderFeature,Stride.Rendering {} + f5a2017030ba4b28784e804807ce7628: !Stride.Rendering.Materials.MaterialRenderFeature,Stride.Rendering {} + 65743b4380f4cc43b2b4bdc23cd0c07c: !Stride.Rendering.Lights.ForwardLightingRenderFeature,Stride.Rendering + LightRenderers: + 7ac2775468f53c4399b2f3f6357c85c9: !Stride.Rendering.Lights.LightAmbientRenderer,Stride.Rendering {} + 7b68f9cd17404a4ba9e5f7df72e3b48d: !Stride.Rendering.Lights.LightDirectionalGroupRenderer,Stride.Rendering {} + 5890e37af0e4bbc2cfdc1de648ff07d4: !Stride.Rendering.Lights.LightPointGroupRenderer,Stride.Rendering {} + ShadowMapRenderer: null + a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6: !Stride.Rendering.UI.UIRenderFeature,Stride.UI + RenderStageSelectors: + d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9: !Stride.Rendering.SimpleGroupToRenderStageSelector,Stride.Rendering + RenderStage: ref!! 31169f13-1e38-4c68-a1e4-1fd9c6d148c3 + EffectName: Test +SharedRenderers: + 60459475d3a3adaf2d1ba5d99913ca75: !Stride.Rendering.Compositing.ForwardRenderer,Stride.Engine + Id: 3e4affe1-a1b8-4e74-ad7d-e038b1407f09 + Clear: + Id: 4bc4b2ca-027e-4e4a-94cb-2912709bef5f + Color*: {R: 0.39215687, G: 0.58431375, B: 0.92941177, A: 1.0} + LightProbes: false + OpaqueRenderStage: ref!! eab72911-5b68-41dd-bfa3-f0ab0e714b4a + TransparentRenderStage: ref!! 31169f13-1e38-4c68-a1e4-1fd9c6d148c3 + ShadowMapRenderStages: + fc4d1e0de5c2b0bbc27bcf96e9a848fd: ref!! 14d1fa9d-90a2-40df-9e6f-bf3fa08f7774 + GBufferRenderStage: null + PostEffects: null + LightShafts: null + VRSettings: + Enabled: false + RequiredApis: {} + Overlays: {} + RequestPassthrough: false + SubsurfaceScatteringBlurEffect: null + MSAALevel: None + MSAAResolver: {} +Game: !Stride.Rendering.Compositing.SceneCameraRenderer,Stride.Engine + Id: 76fe87cf-f574-4ad6-85b8-e9a9586be0e2 + Camera: ref!! 07117963-ef3b-47b6-9344-96aa198edeea + Child: !Stride.Rendering.Compositing.ForwardRenderer,Stride.Engine ref!! 3e4affe1-a1b8-4e74-ad7d-e038b1407f09 + RenderMask: All +SingleView: !Stride.Rendering.Compositing.ForwardRenderer,Stride.Engine ref!! 3e4affe1-a1b8-4e74-ad7d-e038b1407f09 +Editor: !Stride.Rendering.Compositing.ForwardRenderer,Stride.Engine ref!! 3e4affe1-a1b8-4e74-ad7d-e038b1407f09 +BlockPositions: {} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GroundModel.sdpromodel b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GroundModel.sdpromodel new file mode 100644 index 0000000000..5d521c2997 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/GroundModel.sdpromodel @@ -0,0 +1,13 @@ +!ProceduralModelAsset +Id: f5a6b7c8-d9e0-1234-fabc-456789012345 +SerializedVersion: {Stride: 2.0.0.0} +Tags: [] +Type: !PlaneProceduralModel + Size: {X: 10.0, Y: 10.0} + Tessellation: {X: 1, Y: 1} + Normal: UpY + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + UvScale: {X: 1.0, Y: 1.0} + LocalOffset: {X: 0.0, Y: 0.0, Z: 0.0} + MaterialInstance: + Material: null diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/MainScene.sdscene b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/MainScene.sdscene new file mode 100644 index 0000000000..e51329525a --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/MainScene.sdscene @@ -0,0 +1,104 @@ +!SceneAsset +Id: d3e4f5a6-b7c8-9012-defa-234567890123 +SerializedVersion: {Stride: 3.1.0.1} +Tags: [] +ChildrenIds: [] +Offset: {X: 0.0, Y: 0.0, Z: 0.0} +Hierarchy: + RootParts: + - ref!! 10000001-0000-0000-0000-000000000001 + - ref!! 10000002-0000-0000-0000-000000000002 + - ref!! 10000003-0000-0000-0000-000000000003 + - ref!! 10000004-0000-0000-0000-000000000004 + - ref!! 3abf67e6-216c-42ee-a882-9a987a82a2a4 + Parts: + - Entity: + Id: 10000001-0000-0000-0000-000000000001 + Name: Camera + Components: + a100000000000001a100000000000001: !TransformComponent + Id: 20000001-0000-0000-0000-000000000001 + Position: {X: 0.0, Y: 3.0, Z: 6.0} + Rotation: {X: -0.17364818, Y: 0.0, Z: 0.0, W: 0.9848077} + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + Children: {} + a100000000000002a100000000000002: !CameraComponent + Id: 20000002-0000-0000-0000-000000000002 + Name: null + Projection: Perspective + Slot: 07117963-ef3b-47b6-9344-96aa198edeea + - Entity: + Id: 10000002-0000-0000-0000-000000000002 + Name: DirectionalLight + Components: + a100000000000003a100000000000003: !TransformComponent + Id: 20000003-0000-0000-0000-000000000003 + Position: {X: 0.0, Y: 2.0, Z: 0.0} + Rotation: {X: 0.10938163, Y: 0.87571836, Z: 0.26592082, W: -0.38302222} + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + Children: {} + a100000000000004a100000000000004: !LightComponent + Id: 20000004-0000-0000-0000-000000000004 + Type: !LightDirectional + Color: !ColorRgbProvider + Value: {R: 1.0, G: 1.0, B: 1.0} + Shadow: + Size: Large + DepthRange: {} + PartitionMode: !LightDirectionalShadowMap.PartitionLogarithmic {} + ComputeTransmittance: false + BiasParameters: {} + - Entity: + Id: 10000003-0000-0000-0000-000000000003 + Name: TestCube + Components: + a100000000000005a100000000000005: !TransformComponent + Id: 20000005-0000-0000-0000-000000000005 + Position: {X: 0.0, Y: 1.0, Z: 0.0} + Rotation: {X: 0.0, Y: 0.0, Z: 0.0, W: 1.0} + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + Children: + a10000000000000fa10000000000000f: ref!! 20000009-0000-0000-0000-000000000009 + a100000000000006a100000000000006: !ModelComponent + Id: 20000006-0000-0000-0000-000000000006 + Model: e4f5a6b7-c8d9-0123-efab-345678901234:CubeModel + Materials: {} + - Entity: + Id: 10000004-0000-0000-0000-000000000004 + Name: Ground + Components: + a100000000000007a100000000000007: !TransformComponent + Id: 20000007-0000-0000-0000-000000000007 + Position: {X: 0.0, Y: 0.0, Z: 0.0} + Rotation: {X: 0.0, Y: 0.0, Z: 0.0, W: 1.0} + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + Children: {} + a100000000000008a100000000000008: !ModelComponent + Id: 20000008-0000-0000-0000-000000000008 + Model: f5a6b7c8-d9e0-1234-fabc-456789012345:GroundModel + Materials: {} + - Entity: + Id: 10000005-0000-0000-0000-000000000005 + Name: ChildObject + Components: + a100000000000009a100000000000009: !TransformComponent + Id: 20000009-0000-0000-0000-000000000009 + Position: {X: 0.0, Y: 1.2, Z: 0.0} + Rotation: {X: 0.0, Y: 0.0, Z: 0.0, W: 1.0} + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + Children: {} + - Entity: + Id: 3abf67e6-216c-42ee-a882-9a987a82a2a4 + Name: TestUIEntity + Components: + 02a38e7a3c569a29c4902008c4516671: !TransformComponent + Id: 747f4a39-84ab-4454-b6f1-79cc43a6ec78 + Position: {X: 0.0, Y: 0.0, Z: 0.0} + Rotation: {X: 0.0, Y: 0.0, Z: 0.0, W: 1.0} + Scale: {X: 1.0, Y: 1.0, Z: 1.0} + Children: {} + edf184cbdab57c06fe45a3b0e66845dc: !UIComponent + Id: 4440d262-0cd2-485f-bc0b-657f8d83999b + Page: aabd516e-97c9-4e00-b6b9-d77e9faddde8:UI/TestUIPage + Resolution: {X: 800.0, Y: 600.0, Z: 1000.0} + Size: {X: 1.28, Y: 0.72, Z: 1.0} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/UI/TestUIPage.sduipage b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/UI/TestUIPage.sduipage new file mode 100644 index 0000000000..6352d6a430 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Assets/UI/TestUIPage.sduipage @@ -0,0 +1,55 @@ +!UIPageAsset +Id: aabd516e-97c9-4e00-b6b9-d77e9faddde8 +SerializedVersion: {Stride: 2.1.0.1} +Tags: [] +Design: + Resolution: {X: 1280.0, Y: 720.0, Z: 1000.0} +Hierarchy: + RootParts: + - !Grid ref!! ca17bcee-b67e-4d0a-bb03-63dcaf152a69 + Parts: + - UIElement: !Canvas + Id: b89bef8f-7db8-4849-bb29-443e53565950 + DependencyProperties: {} + BackgroundColor: {R: 0, G: 0, B: 0, A: 0} + Width: 100.0 + Height: 40.0 + HorizontalAlignment: Left + VerticalAlignment: Top + Margin: {Left: 50.0, Top: 30.0} + Name: TopLeftButton + Children: {} + - UIElement: !Canvas + Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 + DependencyProperties: {} + BackgroundColor: {R: 0, G: 0, B: 0, A: 0} + Width: 200.0 + Height: 50.0 + HorizontalAlignment: Center + VerticalAlignment: Center + Margin: {} + Name: CenterButton + Children: {} + - UIElement: !Canvas + Id: c2d3e4f5-6789-0abc-def1-234567890abc + DependencyProperties: {} + BackgroundColor: {R: 0, G: 0, B: 0, A: 0} + Width: 150.0 + Height: 60.0 + HorizontalAlignment: Right + VerticalAlignment: Bottom + Margin: {Right: 50.0, Bottom: 30.0} + Name: BottomRightButton + Children: {} + - UIElement: !Grid + Id: ca17bcee-b67e-4d0a-bb03-63dcaf152a69 + DependencyProperties: {} + BackgroundColor: {R: 0, G: 0, B: 0, A: 0} + Margin: {} + Children: + 6e3d90cd1d86beb727a9b80e504ec5a2: !Canvas ref!! b89bef8f-7db8-4849-bb29-443e53565950 + a1b2c3d4e5f67890abcdef1234567890: !Canvas ref!! a1b2c3d4-e5f6-7890-abcd-ef1234567890 + c2d3e4f567890abcdef1234567890abc: !Canvas ref!! c2d3e4f5-6789-0abc-def1-234567890abc + RowDefinitions: {} + ColumnDefinitions: {} + LayerDefinitions: {} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Program.cs b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Program.cs new file mode 100644 index 0000000000..e12946463e --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/Program.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Engine; +using Stride.Engine.Mcp; + +namespace Stride.Engine.Mcp.TestGame +{ + internal static class Program + { + static void Main(string[] args) + { + // Explicitly trigger module initialization (ensures assembly is loaded) + Module.Initialize(); + + using var game = new Game(); + + // The scene, graphics compositor, and game settings are loaded + // from compiled assets (Assets/ folder + TestGame.sdpkg) + game.Run(); + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/TestGame.csproj b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/TestGame.csproj new file mode 100644 index 0000000000..b03b6c221b --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/TestGame.csproj @@ -0,0 +1,42 @@ + + + true + + + + WinExe + net10.0 + win-x64 + + Direct3D11 + Direct3D11 + true + true + $(StrideAssemblyProcessorDefaultOptions) + * + STRIDE_PLATFORM_DESKTOP + + true + true + --compile-property:BuildProjectReferences=false + + + + Properties\SharedAssemblyInfo.cs + + + + + + + + false + false + TargetFramework + true + + + + + + diff --git a/sources/engine/Stride.Engine.Mcp.Tests/TestGame/TestGame.sdpkg b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/TestGame.sdpkg new file mode 100644 index 0000000000..7dad51ff46 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp.Tests/TestGame/TestGame.sdpkg @@ -0,0 +1,22 @@ +!Package +SerializedVersion: {Assets: 3.1.0.0} +Meta: + Name: TestGame + Version: 4.3.0.1 + Authors: [] + Owners: [] + Dependencies: null +AssetFolders: + - Path: !dir Assets +ResourceFolders: [] +OutputGroupDirectories: {} +ExplicitFolders: [] +Bundles: [] +TemplateFolders: [] +RootAssets: + - b1c2d3e4-f5a6-7890-bcde-f12345678901:GameSettings + - c2d3e4f5-a6b7-8901-cdef-123456789012:GraphicsCompositor + - d3e4f5a6-b7c8-9012-defa-234567890123:MainScene + - e4f5a6b7-c8d9-0123-efab-345678901234:CubeModel + - f5a6b7c8-d9e0-1234-fabc-456789012345:GroundModel + - aabd516e-97c9-4e00-b6b9-d77e9faddde8:UI/TestUIPage diff --git a/sources/engine/Stride.Engine.Mcp/GameBridge.cs b/sources/engine/Stride.Engine.Mcp/GameBridge.cs new file mode 100644 index 0000000000..51fdfddeb8 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/GameBridge.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Stride.Input; + +namespace Stride.Engine.Mcp +{ + /// + /// Bridge between MCP tool handlers (Kestrel threads) and the game thread. + /// Injected as a singleton into MCP tool methods. + /// + /// + /// Properties are marked with to prevent + /// the MCP SDK's JSON schema generator from walking into complex engine types + /// that contain ref structs or pointers. + /// + public sealed class GameBridge + { + private readonly Game game; + private readonly GameMcpSystem system; + + [JsonIgnore] + public KeyboardSimulated Keyboard { get; } + + [JsonIgnore] + public MouseSimulated Mouse { get; } + + [JsonIgnore] + public GamePadSimulated GamePad { get; } + + internal GameBridge(Game game, GameMcpSystem system, KeyboardSimulated keyboard, MouseSimulated mouse, GamePadSimulated gamePad) + { + this.game = game; + this.system = system; + Keyboard = keyboard; + Mouse = mouse; + GamePad = gamePad; + } + + public Task RunOnGameThread(Func action, CancellationToken ct = default) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + ct.Register(() => tcs.TrySetCanceled(ct)); + system.EnqueueRequest(new GameThreadRequest + { + Action = g => action(g), + Completion = tcs, + }); + return tcs.Task.ContinueWith(t => (T)t.Result, ct, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + public Task RunOnGameThread(Action action, CancellationToken ct = default) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + ct.Register(() => tcs.TrySetCanceled(ct)); + system.EnqueueRequest(new GameThreadRequest + { + Action = g => + { + action(g); + return null; + }, + Completion = tcs, + }); + return tcs.Task; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/GameMcpSystem.cs b/sources/engine/Stride.Engine.Mcp/GameMcpSystem.cs new file mode 100644 index 0000000000..e0caf6fb6c --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/GameMcpSystem.cs @@ -0,0 +1,172 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Stride.Core; +using Stride.Core.Diagnostics; +using Stride.Games; +using Stride.Input; + +namespace Stride.Engine.Mcp +{ + public class GameMcpSystem : GameSystemBase + { + private static readonly Logger Log = GlobalLogger.GetLogger("GameMcpSystem"); + + private readonly int port; + private readonly ConcurrentQueue pendingRequests = new(); + + private WebApplication webApp; + private CancellationTokenSource cts; + private LogRingBuffer logRingBuffer; + + private InputSourceSimulated inputSourceSimulated; + private KeyboardSimulated keyboardSimulated; + private MouseSimulated mouseSimulated; + private GamePadSimulated gamepadSimulated; + + public GameMcpSystem(IServiceRegistry registry, int port) : base(registry) + { + this.port = port; + DrawOrder = int.MaxValue; + Enabled = true; + Visible = true; + } + + internal void EnqueueRequest(GameThreadRequest request) + { + pendingRequests.Enqueue(request); + } + + public override void Initialize() + { + base.Initialize(); + + var game = (Game)Game; + + // Set up additive input (don't clear existing sources) + var input = Services.GetSafeServiceAs(); + inputSourceSimulated = new InputSourceSimulated(); + input.Sources.Add(inputSourceSimulated); + keyboardSimulated = inputSourceSimulated.AddKeyboard(); + mouseSimulated = inputSourceSimulated.AddMouse(); + gamepadSimulated = inputSourceSimulated.AddGamePad(); + + // Start log capture + logRingBuffer = new LogRingBuffer(); + + // Create bridge + var bridge = new GameBridge(game, this, keyboardSimulated, mouseSimulated, gamepadSimulated); + + // Start Kestrel MCP server + cts = new CancellationTokenSource(); + Task.Run(() => StartMcpServer(bridge), cts.Token); + + Log.Info($"GameMcpSystem initialized, MCP server starting on port {port}"); + } + + private async Task StartMcpServer(GameBridge bridge) + { + try + { + var builder = WebApplication.CreateSlimBuilder(); + builder.WebHost.ConfigureKestrel(options => + { + options.ListenLocalhost(port); + }); + + // Suppress ASP.NET Core console logging + builder.Logging.ClearProviders(); + builder.Logging.SetMinimumLevel(LogLevel.Warning); + + // Register DI services for tool methods + builder.Services.AddSingleton(bridge); + builder.Services.AddSingleton(logRingBuffer); + + builder.Services + .AddMcpServer(options => + { + options.ServerInfo = new() + { + Name = "Stride Game Runtime", + Version = typeof(GameMcpSystem).Assembly.GetName().Version?.ToString() ?? "0.1.0", + }; + }) + .WithHttpTransport() + .WithToolsFromAssembly(typeof(GameMcpSystem).Assembly); + + webApp = builder.Build(); + webApp.MapMcp(); + + Log.Info($"MCP server starting on http://localhost:{port}/sse"); + await webApp.StartAsync(cts.Token); + Log.Info($"MCP server started on http://localhost:{port}/sse"); + + // Wait until cancellation + try + { + await Task.Delay(Timeout.Infinite, cts.Token); + } + catch (OperationCanceledException) { } + } + catch (OperationCanceledException) + { + // Expected during shutdown + } + catch (Exception ex) + { + Log.Error("Failed to start MCP server", ex); + } + } + + public override void Draw(GameTime gameTime) + { + while (pendingRequests.TryDequeue(out var request)) + { + try + { + var result = request.Action((Game)Game); + request.Completion.TrySetResult(result); + } + catch (Exception ex) + { + request.Completion.TrySetException(ex); + } + } + } + + protected override void Destroy() + { + base.Destroy(); + + try + { + cts?.Cancel(); + if (webApp != null) + { + using var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + webApp.StopAsync(stopCts.Token).Wait(TimeSpan.FromSeconds(3)); + webApp.DisposeAsync().AsTask().Wait(TimeSpan.FromSeconds(2)); + } + } + catch (Exception ex) + { + Log.Error("Error stopping MCP server", ex); + } + finally + { + cts?.Dispose(); + logRingBuffer?.Dispose(); + } + + Log.Info("GameMcpSystem destroyed"); + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/GameThreadRequest.cs b/sources/engine/Stride.Engine.Mcp/GameThreadRequest.cs new file mode 100644 index 0000000000..c3c4612a9b --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/GameThreadRequest.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Threading.Tasks; + +namespace Stride.Engine.Mcp +{ + internal class GameThreadRequest + { + public Func Action { get; init; } + public TaskCompletionSource Completion { get; init; } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/LogRingBuffer.cs b/sources/engine/Stride.Engine.Mcp/LogRingBuffer.cs new file mode 100644 index 0000000000..4a1a5cc478 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/LogRingBuffer.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using Stride.Core.Diagnostics; + +namespace Stride.Engine.Mcp +{ + public sealed class LogRingBuffer : IDisposable + { + private readonly LogEntry[] buffer; + private int writeIndex; + private int count; + private readonly object syncLock = new(); + + public LogRingBuffer(int capacity = 500) + { + buffer = new LogEntry[capacity]; + GlobalLogger.GlobalMessageLogged += OnMessageLogged; + } + + private void OnMessageLogged(ILogMessage logMessage) + { + var entry = new LogEntry + { + Timestamp = DateTime.UtcNow, + Module = logMessage.Module, + Level = logMessage.Type, + Message = logMessage.Text, + ExceptionInfo = (logMessage as LogMessage)?.Exception?.ToString(), + }; + + lock (syncLock) + { + buffer[writeIndex] = entry; + writeIndex = (writeIndex + 1) % buffer.Length; + if (count < buffer.Length) + count++; + } + } + + public LogEntry[] GetEntries(int? maxCount = null, LogMessageType? minLevel = null) + { + lock (syncLock) + { + var entries = new List(); + var startIndex = count < buffer.Length ? 0 : writeIndex; + + for (int i = 0; i < count; i++) + { + var idx = (startIndex + i) % buffer.Length; + var entry = buffer[idx]; + if (minLevel.HasValue && entry.Level < minLevel.Value) + continue; + entries.Add(entry); + } + + if (maxCount.HasValue && entries.Count > maxCount.Value) + { + entries = entries.GetRange(entries.Count - maxCount.Value, maxCount.Value); + } + + return entries.ToArray(); + } + } + + public void Dispose() + { + GlobalLogger.GlobalMessageLogged -= OnMessageLogged; + } + } + + public readonly struct LogEntry + { + public DateTime Timestamp { get; init; } + public string Module { get; init; } + public LogMessageType Level { get; init; } + public string Message { get; init; } + public string ExceptionInfo { get; init; } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/McpSettings.cs b/sources/engine/Stride.Engine.Mcp/McpSettings.cs new file mode 100644 index 0000000000..36288a849d --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/McpSettings.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core; +using Stride.Data; + +namespace Stride.Engine.Mcp +{ + [DataContract] + [Display("MCP Server")] + public class McpSettings : Configuration + { + [DataMember(10)] + [Display("Enable MCP Server")] + public bool Enabled { get; set; } = false; + + [DataMember(20)] + [Display("Port")] + public int Port { get; set; } = 5272; + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Module.cs b/sources/engine/Stride.Engine.Mcp/Module.cs new file mode 100644 index 0000000000..7690e7c5d2 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Module.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using Stride.Core; +using Stride.Engine; + +namespace Stride.Engine.Mcp +{ + public class Module + { + private static bool initialized; + + [ModuleInitializer] + public static void Initialize() + { + if (initialized) + return; + initialized = true; + + Game.GameStarted += (sender, args) => + { + var game = (Game)sender; + var settings = game.Settings?.Configurations?.Get() ?? new McpSettings(); + + // Allow environment variable override for testing + var envEnabled = Environment.GetEnvironmentVariable("STRIDE_MCP_GAME_ENABLED"); + if (string.Equals(envEnabled, "true", StringComparison.OrdinalIgnoreCase)) + settings.Enabled = true; + + var envPort = Environment.GetEnvironmentVariable("STRIDE_MCP_GAME_PORT"); + if (int.TryParse(envPort, out var port)) + settings.Port = port; + + if (!settings.Enabled) + return; + + var system = new GameMcpSystem(game.Services, settings.Port); + game.GameSystems.Add(system); + }; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/RuntimeEntitySerializer.cs b/sources/engine/Stride.Engine.Mcp/RuntimeEntitySerializer.cs new file mode 100644 index 0000000000..0e60e47942 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/RuntimeEntitySerializer.cs @@ -0,0 +1,222 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Stride.Core; +using Stride.Core.Mathematics; +using Stride.Core.Serialization; +using Stride.Core.Serialization.Contents; +using Stride.Engine; + +namespace Stride.Engine.Mcp +{ + public static class RuntimeEntitySerializer + { + public static object SerializeEntity(Entity entity, bool includeComponents = true) + { + var result = new Dictionary + { + ["id"] = entity.Id.ToString(), + ["name"] = entity.Name ?? "(unnamed)", + ["enabled"] = true, + }; + + if (entity.Transform != null) + { + result["position"] = SerializeVector3(entity.Transform.Position); + result["rotation"] = SerializeQuaternion(entity.Transform.Rotation); + result["scale"] = SerializeVector3(entity.Transform.Scale); + } + + if (includeComponents) + { + var components = new List(); + foreach (var component in entity.Components) + { + components.Add(SerializeComponent(component)); + } + result["components"] = components; + } + else + { + result["componentSummary"] = entity.Components.Select(c => c.GetType().Name).ToList(); + } + + return result; + } + + public static object SerializeComponent(EntityComponent component) + { + var result = new Dictionary + { + ["type"] = component.GetType().Name, + }; + + try + { + var properties = new Dictionary(); + EnumerateDataMembers(component.GetType(), component, properties, 0); + if (properties.Count > 0) + result["properties"] = properties; + } + catch + { + // If reflection fails, just return the type name + } + + return result; + } + + private static void EnumerateDataMembers(Type type, object instance, Dictionary properties, int depth) + { + if (depth > 3) + return; + + // Get fields and properties with [DataMember] attribute + var members = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.GetCustomAttribute() != null + && m.GetCustomAttribute() == null); + + foreach (var member in members) + { + try + { + object value = null; + if (member is FieldInfo field) + value = field.GetValue(instance); + else if (member is PropertyInfo prop && prop.CanRead) + value = prop.GetValue(instance); + else + continue; + + var name = member.GetCustomAttribute()?.Name ?? member.Name; + properties[name] = SerializeValue(value, depth); + } + catch + { + // Skip members that throw on access + } + } + } + + private static object SerializeValue(object value, int depth) + { + if (value == null) + return null; + + if (depth > 3) + return value.ToString(); + + var type = value.GetType(); + + // Primitives and strings + if (value is float f && (float.IsNaN(f) || float.IsInfinity(f))) + return f.ToString(); + if (value is double d && (double.IsNaN(d) || double.IsInfinity(d))) + return d.ToString(); + if (type.IsPrimitive || type == typeof(string) || type == typeof(decimal)) + return value; + + // Enums + if (type.IsEnum) + return value.ToString(); + + // Guid + if (type == typeof(Guid)) + return value.ToString(); + + // Float vector types + if (value is Vector2 v2) return new { x = v2.X, y = v2.Y }; + if (value is Vector3 v3) return SerializeVector3(v3); + if (value is Vector4 v4) return new { x = v4.X, y = v4.Y, z = v4.Z, w = v4.W }; + if (value is Quaternion q) return SerializeQuaternion(q); + + // Integer vector types + if (value is Int2 i2) return new { x = i2.X, y = i2.Y }; + if (value is Int3 i3) return new { x = i3.X, y = i3.Y, z = i3.Z }; + if (value is Int4 i4) return new { x = i4.X, y = i4.Y, z = i4.Z, w = i4.W }; + + // Colors + if (value is Color c) return new { r = (int)c.R, g = (int)c.G, b = (int)c.B, a = (int)c.A }; + if (value is Color3 c3) return new { r = c3.R, g = c3.G, b = c3.B }; + if (value is Color4 c4) return new { r = c4.R, g = c4.G, b = c4.B, a = c4.A }; + + // Rectangles and sizes + if (value is RectangleF rf) return new { x = rf.X, y = rf.Y, width = rf.Width, height = rf.Height }; + if (value is Rectangle ri) return new { x = ri.X, y = ri.Y, width = ri.Width, height = ri.Height }; + if (value is Size2 s2) return new { width = s2.Width, height = s2.Height }; + if (value is Size2F s2f) return new { width = s2f.Width, height = s2f.Height }; + if (value is Size3 s3) return new { width = s3.Width, height = s3.Height, depth = s3.Depth }; + + // Angle + if (value is AngleSingle angle) return new { degrees = angle.Degrees }; + + // Matrix — too large for structured serialization + if (value is Matrix) return value.ToString(); + + // Asset references — explicit IReference types (AssetReference, UrlReferenceBase) + if (value is IReference reference) + return new { assetRef = reference.Id.ToString(), url = reference.Location?.ToString() }; + + // Asset references — proxy objects with AttachedReference (Model, Material, Texture, etc.) + var attachedRef = AttachedReferenceManager.GetAttachedReference(value); + if (attachedRef != null) + return new { assetRef = attachedRef.Id.ToString(), url = attachedRef.Url }; + + // Entity references + if (value is Entity entityRef) + return new { entityRef = entityRef.Id.ToString(), name = entityRef.Name }; + if (value is EntityComponent comp) + return new { componentRef = comp.GetType().Name, entityId = comp.Entity?.Id.ToString() }; + + // Collections + if (value is IList list) + { + var items = new List(); + var maxItems = Math.Min(list.Count, 20); + for (int i = 0; i < maxItems; i++) + { + items.Add(SerializeValue(list[i], depth + 1)); + } + if (list.Count > 20) + items.Add($"... ({list.Count} items total)"); + return items; + } + + // Dictionaries + if (value is IDictionary dict) + { + var dictResult = new Dictionary(); + int count = 0; + foreach (DictionaryEntry entry in dict) + { + if (count++ >= 20) break; + dictResult[entry.Key?.ToString() ?? "null"] = SerializeValue(entry.Value, depth + 1); + } + return dictResult; + } + + // Complex objects — recurse via [DataMember] reflection + try + { + var properties = new Dictionary(); + EnumerateDataMembers(type, value, properties, depth + 1); + if (properties.Count > 0) + return properties; + } + catch + { + // Fall through to ToString + } + + return value.ToString(); + } + + private static object SerializeVector3(Vector3 v) => new { x = v.X, y = v.Y, z = v.Z }; + private static object SerializeQuaternion(Quaternion q) => new { x = q.X, y = q.Y, z = q.Z, w = q.W }; + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Stride.Engine.Mcp.csproj b/sources/engine/Stride.Engine.Mcp/Stride.Engine.Mcp.csproj new file mode 100644 index 0000000000..9fb3127458 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Stride.Engine.Mcp.csproj @@ -0,0 +1,26 @@ + + + true + + + + true + true + $(StrideAssemblyProcessorDefaultOptions) + * + + + + Properties\SharedAssemblyInfo.cs + + + + + + + + + + + + diff --git a/sources/engine/Stride.Engine.Mcp/Tools/CaptureScreenshotTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/CaptureScreenshotTool.cs new file mode 100644 index 0000000000..49b7c034db --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/CaptureScreenshotTool.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using Stride.Graphics; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class CaptureScreenshotTool + { + [McpServerTool(Name = "capture_screenshot"), Description("Captures a screenshot of the game's back buffer and returns it as a base64-encoded PNG image.")] + public static async Task> CaptureScreenshot( + GameBridge bridge, + CancellationToken cancellationToken = default) + { + var base64 = await bridge.RunOnGameThread(game => + { + var backBuffer = game.GraphicsDevice?.Presenter?.BackBuffer; + if (backBuffer == null) + return null; + + using var image = backBuffer.GetDataAsImage(game.GraphicsContext.CommandList); + using var memoryStream = new MemoryStream(); + image.Save(memoryStream, ImageFileType.Png); + return Convert.ToBase64String(memoryStream.ToArray()); + }, cancellationToken); + + if (base64 == null) + { + return new ContentBlock[] + { + new TextContentBlock { Text = "Error: Back buffer is not available" }, + }; + } + + return new ContentBlock[] + { + new ImageContentBlock + { + Data = base64, + MimeType = "image/png", + }, + }; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Tools/DescribeViewportTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/DescribeViewportTool.cs new file mode 100644 index 0000000000..7716067781 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/DescribeViewportTool.cs @@ -0,0 +1,226 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Mathematics; +using Stride.Engine; +using Stride.Engine.Processors; +using Stride.Graphics; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class DescribeViewportTool + { + [McpServerTool(Name = "describe_viewport"), Description("Returns a list of entities visible in the game viewport with their projected screen coordinates (normalized 0-1). This helps understand what is rendered even when materials or lighting make the image hard to interpret. Use alongside capture_screenshot to correlate visual output with entity positions.")] + public static async Task DescribeViewport( + GameBridge bridge, + [Description("Maximum number of entities to return (default 100)")] int maxEntities = 100, + CancellationToken cancellationToken = default) + { + var result = await bridge.RunOnGameThread(game => + { + var rootScene = game.SceneSystem?.SceneInstance?.RootScene; + if (rootScene == null) + return (object)new { error = "No scene loaded" }; + + // Find the active camera + CameraComponent activeCamera = null; + FindActiveCamera(rootScene, ref activeCamera); + + if (activeCamera == null) + return (object)new { error = "No active camera found in the scene" }; + + // Get the viewport + var viewport = game.GraphicsContext.CommandList.Viewport; + var viewProjection = activeCamera.ViewProjectionMatrix; + + // Use a normalized viewport for 0-1 screen coordinates + var normalizedViewport = new Viewport(0, 0, 1, 1); + + // Collect all entities with their screen projections + var entities = new List(); + CollectEntities(rootScene, activeCamera, normalizedViewport, entities); + + // Sort by distance to camera (nearest first), then cap + entities.Sort((a, b) => a.DistanceToCamera.CompareTo(b.DistanceToCamera)); + if (entities.Count > maxEntities) + entities.RemoveRange(maxEntities, entities.Count - maxEntities); + + // Extract camera world position from view matrix + var viewMatrix = activeCamera.ViewMatrix; + Matrix.Invert(ref viewMatrix, out var cameraWorldMatrix); + var camPos = cameraWorldMatrix.TranslationVector; + + return (object)new Dictionary + { + ["camera"] = new Dictionary + { + ["position"] = new { x = Math.Round(camPos.X, 2), y = Math.Round(camPos.Y, 2), z = Math.Round(camPos.Z, 2) }, + ["projection"] = activeCamera.Projection == CameraProjectionMode.Perspective ? "perspective" : "orthographic", + ["fieldOfViewDegrees"] = Math.Round(activeCamera.VerticalFieldOfView, 1), + ["nearPlane"] = Math.Round(activeCamera.NearClipPlane, 3), + ["farPlane"] = Math.Round(activeCamera.FarClipPlane, 1), + ["aspectRatio"] = Math.Round(activeCamera.AspectRatio, 3), + }, + ["totalEntitiesInScene"] = CountEntities(rootScene), + ["visibleEntityCount"] = entities.Count(e => e.IsVisible), + ["entities"] = entities.Select(e => (object)new Dictionary + { + ["id"] = e.Id, + ["name"] = e.Name, + ["worldPosition"] = new { x = Math.Round(e.WorldPosition.X, 2), y = Math.Round(e.WorldPosition.Y, 2), z = Math.Round(e.WorldPosition.Z, 2) }, + ["screenPosition"] = e.IsVisible + ? (object)new { x = Math.Round(e.ScreenX, 3), y = Math.Round(e.ScreenY, 3) } + : null, + ["distanceToCamera"] = Math.Round(e.DistanceToCamera, 2), + ["isVisible"] = e.IsVisible, + ["components"] = e.Components, + }).ToList(), + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static void FindActiveCamera(Scene scene, ref CameraComponent camera) + { + foreach (var entity in scene.Entities) + { + FindCameraInEntity(entity, ref camera); + if (camera != null) return; + } + + foreach (var child in scene.Children) + { + FindActiveCamera(child, ref camera); + if (camera != null) return; + } + } + + private static void FindCameraInEntity(Entity entity, ref CameraComponent camera) + { + var cam = entity.Get(); + if (cam != null && cam.Enabled) + { + camera = cam; + return; + } + + if (entity.Transform != null) + { + foreach (var child in entity.Transform.Children) + { + if (child.Entity != null) + { + FindCameraInEntity(child.Entity, ref camera); + if (camera != null) return; + } + } + } + } + + private static void CollectEntities(Scene scene, CameraComponent camera, Viewport viewport, List results) + { + foreach (var entity in scene.Entities) + { + CollectEntityRecursive(entity, camera, viewport, results); + } + + foreach (var child in scene.Children) + { + CollectEntities(child, camera, viewport, results); + } + } + + private static void CollectEntityRecursive(Entity entity, CameraComponent camera, Viewport viewport, List results) + { + var worldPos = entity.Transform.WorldMatrix.TranslationVector; + + // Get camera position for distance calculation + var viewMatrix = camera.ViewMatrix; + Matrix.Invert(ref viewMatrix, out var cameraWorldMatrix); + var camPos = cameraWorldMatrix.TranslationVector; + var distance = Vector3.Distance(worldPos, camPos); + + // Project to screen space + var screenPos = viewport.Project(worldPos, camera.ProjectionMatrix, camera.ViewMatrix, Matrix.Identity); + + // Check visibility: in front of camera and within screen bounds + bool isVisible = screenPos.Z >= 0 && screenPos.Z <= 1 + && screenPos.X >= -0.5f && screenPos.X <= 1.5f + && screenPos.Y >= -0.5f && screenPos.Y <= 1.5f; + + results.Add(new EntityScreenInfo + { + Id = entity.Id.ToString(), + Name = entity.Name ?? "(unnamed)", + WorldPosition = worldPos, + ScreenX = screenPos.X, + ScreenY = screenPos.Y, + DistanceToCamera = distance, + IsVisible = isVisible, + Components = entity.Components.Select(c => c.GetType().Name).ToList(), + }); + + // Recurse into children + if (entity.Transform != null) + { + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + { + CollectEntityRecursive(childTransform.Entity, camera, viewport, results); + } + } + } + } + + private static int CountEntities(Scene scene) + { + int count = 0; + foreach (var entity in scene.Entities) + { + count += CountEntityRecursive(entity); + } + foreach (var child in scene.Children) + { + count += CountEntities(child); + } + return count; + } + + private static int CountEntityRecursive(Entity entity) + { + int count = 1; + if (entity.Transform != null) + { + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + count += CountEntityRecursive(childTransform.Entity); + } + } + return count; + } + + private class EntityScreenInfo + { + public string Id; + public string Name; + public Vector3 WorldPosition; + public float ScreenX; + public float ScreenY; + public float DistanceToCamera; + public bool IsVisible; + public List Components; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Tools/FocusElementTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/FocusElementTool.cs new file mode 100644 index 0000000000..1f9cdde233 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/FocusElementTool.cs @@ -0,0 +1,290 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Mathematics; +using Stride.Engine; +using Stride.Graphics; +using Stride.Rendering; +using Stride.UI; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class FocusElementTool + { + [McpServerTool(Name = "focus_element"), Description("Moves the simulated mouse pointer to a UI element or scene entity's screen position. For UI elements, searches all UIComponents in the scene by element name. For entities, projects their world position to screen space using the active camera.")] + public static async Task FocusElement( + GameBridge bridge, + [Description("Target type: 'ui' for UI elements, 'entity' for scene entities")] string target, + [Description("UI element name (for target='ui')")] string elementName = null, + [Description("Entity GUID (for target='entity')")] string entityId = null, + [Description("Entity name (for target='entity')")] string entityName = null, + CancellationToken cancellationToken = default) + { + try + { + var result = await bridge.RunOnGameThread(game => + { + switch (target?.ToLowerInvariant()) + { + case "ui": + return FocusUIElement(game, bridge, elementName); + case "entity": + return FocusEntity(game, bridge, entityId, entityName); + default: + return (object)new { error = $"Invalid target type: {target}. Use 'ui' or 'entity'." }; + } + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { error = $"Focus element failed: {ex.Message}" }); + } + } + + private static object FocusUIElement(Game game, GameBridge bridge, string elementName) + { + if (string.IsNullOrEmpty(elementName)) + return new { error = "elementName is required for UI targeting" }; + + var rootScene = game.SceneSystem?.SceneInstance?.RootScene; + if (rootScene == null) + return new { error = "No scene loaded" }; + + // Search all entities with UIComponent for the named element + UIElement foundElement = null; + UIComponent foundComponent = null; + + SearchUIElementInScene(rootScene, elementName, ref foundElement, ref foundComponent); + + if (foundElement == null || foundComponent == null) + return new { error = $"UI element not found: {elementName}" }; + + // Map through UI resolution to screen space + var resolution = foundComponent.Resolution; + var backBuffer = game.GraphicsDevice?.Presenter?.BackBuffer; + if (backBuffer == null) + return new { error = "Back buffer not available" }; + + var screenWidth = (float)backBuffer.Width; + var screenHeight = (float)backBuffer.Height; + + // Adapt the virtual resolution based on ResolutionStretch mode + var virtualResolution = new Vector2(resolution.X, resolution.Y); + switch (foundComponent.ResolutionStretch) + { + case ResolutionStretch.FixedWidthAdaptableHeight: + virtualResolution.Y = virtualResolution.X * screenHeight / screenWidth; + break; + case ResolutionStretch.FixedHeightAdaptableWidth: + virtualResolution.X = virtualResolution.Y * screenWidth / screenHeight; + break; + } + + // The element's WorldMatrix is in centered UI world space, where the root + // is translated by -virtualResolution/2 (see UIRenderFeature). So WorldMatrix + // has the element's center offset by -virtualResolution/2 from virtual coords. + // We undo this to get the position in virtual resolution space (0,0 = top-left). + var centerWorld = foundElement.WorldMatrix.TranslationVector; + var virtualPosX = centerWorld.X + virtualResolution.X / 2f; + var virtualPosY = centerWorld.Y + virtualResolution.Y / 2f; + + var normalizedX = virtualPosX / virtualResolution.X; + var normalizedY = virtualPosY / virtualResolution.Y; + + // Clamp to 0-1 range + normalizedX = MathUtil.Clamp(normalizedX, 0f, 1f); + normalizedY = MathUtil.Clamp(normalizedY, 0f, 1f); + + var screenX = normalizedX * screenWidth; + var screenY = normalizedY * screenHeight; + + // Move the simulated mouse + bridge.Mouse.SetPosition(new Vector2(normalizedX, normalizedY)); + + return new + { + success = true, + screenX, + screenY, + normalizedX, + normalizedY, + }; + } + + private static void SearchUIElementInScene(Scene scene, string elementName, ref UIElement foundElement, ref UIComponent foundComponent) + { + foreach (var entity in scene.Entities) + { + SearchUIElementInEntity(entity, elementName, ref foundElement, ref foundComponent); + if (foundElement != null) return; + } + foreach (var child in scene.Children) + { + SearchUIElementInScene(child, elementName, ref foundElement, ref foundComponent); + if (foundElement != null) return; + } + } + + private static void SearchUIElementInEntity(Entity entity, string elementName, ref UIElement foundElement, ref UIComponent foundComponent) + { + var uiComponent = entity.Get(); + if (uiComponent?.Page?.RootElement != null) + { + var element = uiComponent.Page.RootElement.FindName(elementName); + if (element != null) + { + foundElement = element; + foundComponent = uiComponent; + return; + } + } + + if (entity.Transform != null) + { + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + { + SearchUIElementInEntity(childTransform.Entity, elementName, ref foundElement, ref foundComponent); + if (foundElement != null) return; + } + } + } + } + + private static object FocusEntity(Game game, GameBridge bridge, string entityId, string entityName) + { + try + { + if (string.IsNullOrEmpty(entityId) && string.IsNullOrEmpty(entityName)) + return new { error = "Either entityId or entityName is required for entity targeting" }; + + var rootScene = game.SceneSystem?.SceneInstance?.RootScene; + if (rootScene == null) + return new { error = "No scene loaded" }; + + Entity found = null; + if (!string.IsNullOrEmpty(entityId) && Guid.TryParse(entityId, out var guid)) + { + found = GetEntityTool.FindEntityById(rootScene, guid); + } + else if (!string.IsNullOrEmpty(entityName)) + { + found = GetEntityTool.FindEntityByName(rootScene, entityName); + } + + if (found == null) + return new { error = $"Entity not found: {entityId ?? entityName}" }; + + // Get entity world position + var worldPos = found.Transform.WorldMatrix.TranslationVector; + + // Find the active camera (must be enabled) + var camera = FindActiveCamera(rootScene); + if (camera == null) + return new { error = "No enabled camera found in scene" }; + + // Project world position to screen space + var backBuffer = game.GraphicsDevice?.Presenter?.BackBuffer; + if (backBuffer == null) + return new { error = "Back buffer not available" }; + + var viewport = new Viewport(0, 0, backBuffer.Width, backBuffer.Height); + + // Update camera matrices with the correct screen aspect ratio + var screenAspectRatio = (float)backBuffer.Width / backBuffer.Height; + camera.Update(screenAspectRatio); + var viewMatrix = camera.ViewMatrix; + var projectionMatrix = camera.ProjectionMatrix; + + var screenPos = viewport.Project(worldPos, projectionMatrix, viewMatrix, Matrix.Identity); + + // Check for invalid projection results (NaN/Infinity) + if (float.IsNaN(screenPos.X) || float.IsInfinity(screenPos.X) || + float.IsNaN(screenPos.Y) || float.IsInfinity(screenPos.Y)) + { + return new { error = "Could not project entity position to screen (invalid camera matrices)" }; + } + + // Check depth — Z outside [0,1] means the entity is behind the camera or beyond the far plane + if (screenPos.Z < 0 || screenPos.Z > 1) + { + return new { error = $"Entity is behind the camera or outside the view frustum (depth: {screenPos.Z:F3})" }; + } + + // Normalize to 0-1 + var normalizedX = screenPos.X / backBuffer.Width; + var normalizedY = screenPos.Y / backBuffer.Height; + + // Check if on screen + if (normalizedX < 0 || normalizedX > 1 || normalizedY < 0 || normalizedY > 1) + { + return new { error = $"Entity is off-screen (normalized: {normalizedX:F3}, {normalizedY:F3})" }; + } + + // Move the simulated mouse + bridge.Mouse.SetPosition(new Vector2(normalizedX, normalizedY)); + + return new + { + success = true, + screenX = screenPos.X, + screenY = screenPos.Y, + normalizedX, + normalizedY, + }; + } + catch (Exception ex) + { + return new { error = $"Failed to focus entity: {ex.Message}" }; + } + } + + private static CameraComponent FindActiveCamera(Scene scene) + { + foreach (var entity in scene.Entities) + { + var camera = FindCameraInEntity(entity); + if (camera != null) + return camera; + } + foreach (var child in scene.Children) + { + var camera = FindActiveCamera(child); + if (camera != null) + return camera; + } + return null; + } + + private static CameraComponent FindCameraInEntity(Entity entity) + { + var camera = entity.Get(); + if (camera != null && camera.Enabled) + return camera; + + if (entity.Transform != null) + { + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + { + var found = FindCameraInEntity(childTransform.Entity); + if (found != null) + return found; + } + } + } + return null; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Tools/GetEntityTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/GetEntityTool.cs new file mode 100644 index 0000000000..ed6ac8b53a --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/GetEntityTool.cs @@ -0,0 +1,128 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class GetEntityTool + { + [McpServerTool(Name = "get_entity"), Description("Returns detailed information about a specific entity including all its components and their serialized properties. Search by entity ID (GUID) or name (first match).")] + public static async Task GetEntity( + GameBridge bridge, + [Description("Entity GUID to look up")] string entityId = null, + [Description("Entity name to look up (first match)")] string entityName = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(entityId) && string.IsNullOrEmpty(entityName)) + { + return JsonSerializer.Serialize(new { error = "Either entityId or entityName must be provided" }); + } + + var result = await bridge.RunOnGameThread(game => + { + var rootScene = game.SceneSystem?.SceneInstance?.RootScene; + if (rootScene == null) + return new { error = "No scene loaded" }; + + Entity found = null; + + if (!string.IsNullOrEmpty(entityId) && Guid.TryParse(entityId, out var guid)) + { + found = FindEntityById(rootScene, guid); + } + else if (!string.IsNullOrEmpty(entityName)) + { + found = FindEntityByName(rootScene, entityName); + } + + if (found == null) + { + return (object)new { error = $"Entity not found: {entityId ?? entityName}" }; + } + + return RuntimeEntitySerializer.SerializeEntity(found, includeComponents: true); + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + internal static Entity FindEntityById(Scene scene, Guid id) + { + foreach (var entity in scene.Entities) + { + var found = FindEntityByIdRecursive(entity, id); + if (found != null) + return found; + } + foreach (var child in scene.Children) + { + var found = FindEntityById(child, id); + if (found != null) + return found; + } + return null; + } + + private static Entity FindEntityByIdRecursive(Entity entity, Guid id) + { + if (entity.Id == id) + return entity; + if (entity.Transform != null) + { + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + { + var found = FindEntityByIdRecursive(childTransform.Entity, id); + if (found != null) + return found; + } + } + } + return null; + } + + internal static Entity FindEntityByName(Scene scene, string name) + { + foreach (var entity in scene.Entities) + { + var found = FindEntityByNameRecursive(entity, name); + if (found != null) + return found; + } + foreach (var child in scene.Children) + { + var found = FindEntityByName(child, name); + if (found != null) + return found; + } + return null; + } + + private static Entity FindEntityByNameRecursive(Entity entity, string name) + { + if (string.Equals(entity.Name, name, StringComparison.OrdinalIgnoreCase)) + return entity; + if (entity.Transform != null) + { + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + { + var found = FindEntityByNameRecursive(childTransform.Entity, name); + if (found != null) + return found; + } + } + } + return null; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Tools/GetGameStatusTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/GetGameStatusTool.cs new file mode 100644 index 0000000000..26273f826d --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/GetGameStatusTool.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.ComponentModel; +using System.Diagnostics; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class GetGameStatusTool + { + [McpServerTool(Name = "get_game_status"), Description("Returns the current game status including state, FPS, resolution, active scene, entity count, and uptime.")] + public static async Task GetGameStatus( + GameBridge bridge, + CancellationToken cancellationToken) + { + var status = await bridge.RunOnGameThread(game => + { + var sceneSystem = game.SceneSystem; + var rootScene = sceneSystem?.SceneInstance?.RootScene; + + var entityCount = 0; + if (rootScene != null) + { + entityCount = CountEntities(rootScene); + } + + var graphicsDevice = game.GraphicsDevice; + var presenter = graphicsDevice?.Presenter; + + return new + { + status = game.IsRunning ? "running" : "stopped", + fps = game.UpdateTime.FramePerSecond, + resolution = presenter?.BackBuffer != null + ? new { width = presenter.BackBuffer.Width, height = presenter.BackBuffer.Height } + : null, + activeSceneUrl = game.Settings?.DefaultSceneUrl ?? "(unknown)", + entityCount, + totalLoadedScenes = rootScene != null ? 1 + CountChildScenes(rootScene) : 0, + uptimeSeconds = game.UpdateTime.Total.TotalSeconds, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(status, new JsonSerializerOptions { WriteIndented = true }); + } + + private static int CountEntities(Scene scene) + { + var count = scene.Entities.Count; + foreach (var child in scene.Children) + { + count += CountEntities(child); + } + return count; + } + + private static int CountChildScenes(Scene scene) + { + var count = scene.Children.Count; + foreach (var child in scene.Children) + { + count += CountChildScenes(child); + } + return count; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Tools/GetLogsTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/GetLogsTool.cs new file mode 100644 index 0000000000..4dc175bef9 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/GetLogsTool.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Diagnostics; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class GetLogsTool + { + [McpServerTool(Name = "get_logs"), Description("Returns recent log entries from the game. Can filter by minimum log level and limit the number of entries returned.")] + public static Task GetLogs( + LogRingBuffer logRingBuffer, + [Description("Maximum number of log entries to return (default: 100)")] int maxCount = 100, + [Description("Minimum log level filter: Debug, Info, Warning, Error, or Fatal")] string minLevel = null, + CancellationToken cancellationToken = default) + { + LogMessageType? minLevelParsed = null; + if (!string.IsNullOrEmpty(minLevel)) + { + if (Enum.TryParse(minLevel, ignoreCase: true, out var parsed)) + { + minLevelParsed = parsed; + } + else + { + return Task.FromResult(JsonSerializer.Serialize(new { error = $"Invalid log level: {minLevel}. Valid values: Debug, Info, Warning, Error, Fatal" })); + } + } + + var entries = logRingBuffer.GetEntries(maxCount, minLevelParsed); + + var result = entries.Select(e => new + { + timestamp = e.Timestamp.ToString("O"), + module = e.Module, + level = e.Level.ToString(), + message = e.Message, + exceptionInfo = e.ExceptionInfo, + }).ToArray(); + + return Task.FromResult(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true })); + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Tools/GetSceneEntitiesTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/GetSceneEntitiesTool.cs new file mode 100644 index 0000000000..2c93cebd9c --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/GetSceneEntitiesTool.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class GetSceneEntitiesTool + { + [McpServerTool(Name = "get_scene_entities"), Description("Returns the entity hierarchy of the active scene as a tree. Each node includes id, name, enabled state, position, child count, component summary, and children.")] + public static async Task GetSceneEntities( + GameBridge bridge, + [Description("Maximum depth of the hierarchy tree (default: 3)")] int maxDepth = 3, + CancellationToken cancellationToken = default) + { + var result = await bridge.RunOnGameThread(game => + { + var rootScene = game.SceneSystem?.SceneInstance?.RootScene; + if (rootScene == null) + return new { error = "No scene loaded" }; + + var entities = new List(); + foreach (var entity in rootScene.Entities) + { + entities.Add(SerializeEntityNode(entity, 0, maxDepth)); + } + + return (object)new + { + sceneName = rootScene.Name ?? "(root)", + entities, + }; + }, cancellationToken); + + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + private static object SerializeEntityNode(Entity entity, int currentDepth, int maxDepth) + { + var node = new Dictionary + { + ["id"] = entity.Id.ToString(), + ["name"] = entity.Name ?? "(unnamed)", + ["enabled"] = true, + }; + + if (entity.Transform != null) + { + node["position"] = new + { + x = entity.Transform.Position.X, + y = entity.Transform.Position.Y, + z = entity.Transform.Position.Z, + }; + } + + node["componentSummary"] = entity.Components.Select(c => c.GetType().Name).ToList(); + node["childCount"] = entity.Transform?.Children.Count ?? 0; + + if (currentDepth < maxDepth && entity.Transform != null) + { + var children = new List(); + foreach (var childTransform in entity.Transform.Children) + { + if (childTransform.Entity != null) + { + children.Add(SerializeEntityNode(childTransform.Entity, currentDepth + 1, maxDepth)); + } + } + node["children"] = children; + } + + return node; + } + } +} diff --git a/sources/engine/Stride.Engine.Mcp/Tools/SimulateInputTool.cs b/sources/engine/Stride.Engine.Mcp/Tools/SimulateInputTool.cs new file mode 100644 index 0000000000..e0e855f252 --- /dev/null +++ b/sources/engine/Stride.Engine.Mcp/Tools/SimulateInputTool.cs @@ -0,0 +1,225 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Stride.Core.Mathematics; +using Stride.Input; + +namespace Stride.Engine.Mcp.Tools +{ + [McpServerToolType] + public sealed class SimulateInputTool + { + [McpServerTool(Name = "simulate_input"), Description("Injects simulated input into the running game. Supports keyboard, mouse, and gamepad actions. Press actions (key_press, mouse_click, gamepad_button_press) perform down + wait one frame + up.")] + public static async Task SimulateInput( + GameBridge bridge, + [Description("Action to perform: key_down, key_up, key_press, mouse_move, mouse_down, mouse_up, mouse_click, gamepad_button_down, gamepad_button_up, gamepad_button_press, gamepad_axis")] string action, + [Description("Key name for keyboard actions (e.g. Space, W, Left, Return)")] string key = null, + [Description("Mouse button for mouse actions: Left, Right, Middle")] string button = null, + [Description("Normalized position for mouse_move as 'x,y' (0-1 range)")] string position = null, + [Description("Gamepad button name (e.g. A, B, X, Y, LeftShoulder, Start, PadUp)")] string gamepadButton = null, + [Description("Gamepad axis name (e.g. LeftThumbX, LeftThumbY, RightTrigger)")] string gamepadAxis = null, + [Description("Axis value from -1.0 to 1.0 for thumbsticks, 0.0 to 1.0 for triggers")] float axisValue = 0f, + CancellationToken cancellationToken = default) + { + try + { + switch (action?.ToLowerInvariant()) + { + case "key_down": + return await HandleKeyDown(bridge, key, cancellationToken); + case "key_up": + return await HandleKeyUp(bridge, key, cancellationToken); + case "key_press": + return await HandleKeyPress(bridge, key, cancellationToken); + case "mouse_move": + return await HandleMouseMove(bridge, position, cancellationToken); + case "mouse_down": + return await HandleMouseDown(bridge, button, cancellationToken); + case "mouse_up": + return await HandleMouseUp(bridge, button, cancellationToken); + case "mouse_click": + return await HandleMouseClick(bridge, button, cancellationToken); + case "gamepad_button_down": + return await HandleGamepadButtonDown(bridge, gamepadButton, cancellationToken); + case "gamepad_button_up": + return await HandleGamepadButtonUp(bridge, gamepadButton, cancellationToken); + case "gamepad_button_press": + return await HandleGamepadButtonPress(bridge, gamepadButton, cancellationToken); + case "gamepad_axis": + return await HandleGamepadAxis(bridge, gamepadAxis, axisValue, cancellationToken); + default: + return JsonSerializer.Serialize(new { error = $"Unknown action: {action}" }); + } + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { error = ex.Message }); + } + } + + private static async Task HandleKeyDown(GameBridge bridge, string key, CancellationToken ct) + { + if (!TryParseKey(key, out var parsedKey)) + return JsonSerializer.Serialize(new { error = $"Invalid key: {key}" }); + + await bridge.RunOnGameThread(_ => bridge.Keyboard.SimulateDown(parsedKey), ct); + return JsonSerializer.Serialize(new { success = true, action = "key_down", key }); + } + + private static async Task HandleKeyUp(GameBridge bridge, string key, CancellationToken ct) + { + if (!TryParseKey(key, out var parsedKey)) + return JsonSerializer.Serialize(new { error = $"Invalid key: {key}" }); + + await bridge.RunOnGameThread(_ => bridge.Keyboard.SimulateUp(parsedKey), ct); + return JsonSerializer.Serialize(new { success = true, action = "key_up", key }); + } + + private static async Task HandleKeyPress(GameBridge bridge, string key, CancellationToken ct) + { + if (!TryParseKey(key, out var parsedKey)) + return JsonSerializer.Serialize(new { error = $"Invalid key: {key}" }); + + // Down on one frame + await bridge.RunOnGameThread(_ => bridge.Keyboard.SimulateDown(parsedKey), ct); + // Up on next frame + await bridge.RunOnGameThread(_ => bridge.Keyboard.SimulateUp(parsedKey), ct); + return JsonSerializer.Serialize(new { success = true, action = "key_press", key }); + } + + private static async Task HandleMouseMove(GameBridge bridge, string position, CancellationToken ct) + { + if (!TryParsePosition(position, out var pos)) + return JsonSerializer.Serialize(new { error = $"Invalid position format: {position}. Expected 'x,y' with values 0-1." }); + + await bridge.RunOnGameThread(_ => bridge.Mouse.SetPosition(pos), ct); + return JsonSerializer.Serialize(new { success = true, action = "mouse_move", x = pos.X, y = pos.Y }); + } + + private static async Task HandleMouseDown(GameBridge bridge, string button, CancellationToken ct) + { + if (!TryParseMouseButton(button, out var parsedButton)) + return JsonSerializer.Serialize(new { error = $"Invalid mouse button: {button}" }); + + await bridge.RunOnGameThread(_ => bridge.Mouse.SimulateMouseDown(parsedButton), ct); + return JsonSerializer.Serialize(new { success = true, action = "mouse_down", button }); + } + + private static async Task HandleMouseUp(GameBridge bridge, string button, CancellationToken ct) + { + if (!TryParseMouseButton(button, out var parsedButton)) + return JsonSerializer.Serialize(new { error = $"Invalid mouse button: {button}" }); + + await bridge.RunOnGameThread(_ => bridge.Mouse.SimulateMouseUp(parsedButton), ct); + return JsonSerializer.Serialize(new { success = true, action = "mouse_up", button }); + } + + private static async Task HandleMouseClick(GameBridge bridge, string button, CancellationToken ct) + { + if (!TryParseMouseButton(button, out var parsedButton)) + return JsonSerializer.Serialize(new { error = $"Invalid mouse button: {button}" }); + + // Down on one frame + await bridge.RunOnGameThread(_ => bridge.Mouse.SimulateMouseDown(parsedButton), ct); + // Up on next frame + await bridge.RunOnGameThread(_ => bridge.Mouse.SimulateMouseUp(parsedButton), ct); + return JsonSerializer.Serialize(new { success = true, action = "mouse_click", button }); + } + + private static async Task HandleGamepadButtonDown(GameBridge bridge, string gamepadButton, CancellationToken ct) + { + if (!TryParseGamePadButton(gamepadButton, out var parsedButton)) + return JsonSerializer.Serialize(new { error = $"Invalid gamepad button: {gamepadButton}" }); + + await bridge.RunOnGameThread(_ => bridge.GamePad.SetButton(parsedButton, true), ct); + return JsonSerializer.Serialize(new { success = true, action = "gamepad_button_down", gamepadButton }); + } + + private static async Task HandleGamepadButtonUp(GameBridge bridge, string gamepadButton, CancellationToken ct) + { + if (!TryParseGamePadButton(gamepadButton, out var parsedButton)) + return JsonSerializer.Serialize(new { error = $"Invalid gamepad button: {gamepadButton}" }); + + await bridge.RunOnGameThread(_ => bridge.GamePad.SetButton(parsedButton, false), ct); + return JsonSerializer.Serialize(new { success = true, action = "gamepad_button_up", gamepadButton }); + } + + private static async Task HandleGamepadButtonPress(GameBridge bridge, string gamepadButton, CancellationToken ct) + { + if (!TryParseGamePadButton(gamepadButton, out var parsedButton)) + return JsonSerializer.Serialize(new { error = $"Invalid gamepad button: {gamepadButton}" }); + + // Down on one frame + await bridge.RunOnGameThread(_ => bridge.GamePad.SetButton(parsedButton, true), ct); + // Up on next frame + await bridge.RunOnGameThread(_ => bridge.GamePad.SetButton(parsedButton, false), ct); + return JsonSerializer.Serialize(new { success = true, action = "gamepad_button_press", gamepadButton }); + } + + private static async Task HandleGamepadAxis(GameBridge bridge, string gamepadAxis, float axisValue, CancellationToken ct) + { + if (!TryParseGamePadAxis(gamepadAxis, out var parsedAxis)) + return JsonSerializer.Serialize(new { error = $"Invalid gamepad axis: {gamepadAxis}" }); + + await bridge.RunOnGameThread(_ => bridge.GamePad.SetAxis(parsedAxis, axisValue), ct); + return JsonSerializer.Serialize(new { success = true, action = "gamepad_axis", gamepadAxis, axisValue }); + } + + private static bool TryParseKey(string key, out Keys result) + { + result = default; + if (string.IsNullOrEmpty(key)) + return false; + return Enum.TryParse(key, ignoreCase: true, out result); + } + + private static bool TryParseMouseButton(string button, out MouseButton result) + { + result = default; + if (string.IsNullOrEmpty(button)) + return false; + return Enum.TryParse(button, ignoreCase: true, out result); + } + + private static bool TryParseGamePadButton(string button, out GamePadButton result) + { + result = default; + if (string.IsNullOrEmpty(button)) + return false; + return Enum.TryParse(button, ignoreCase: true, out result); + } + + private static bool TryParseGamePadAxis(string axis, out GamePadAxis result) + { + result = default; + if (string.IsNullOrEmpty(axis)) + return false; + return Enum.TryParse(axis, ignoreCase: true, out result); + } + + private static bool TryParsePosition(string position, out Vector2 result) + { + result = default; + if (string.IsNullOrEmpty(position)) + return false; + + var parts = position.Split(','); + if (parts.Length != 2) + return false; + + if (!float.TryParse(parts[0].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var x)) + return false; + if (!float.TryParse(parts[1].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var y)) + return false; + + result = new Vector2(x, y); + return true; + } + } +} diff --git a/sources/engine/Stride.Engine/Engine/ModelComponent.cs b/sources/engine/Stride.Engine/Engine/ModelComponent.cs index de85a667ec..c145b625e3 100644 --- a/sources/engine/Stride.Engine/Engine/ModelComponent.cs +++ b/sources/engine/Stride.Engine/Engine/ModelComponent.cs @@ -124,6 +124,9 @@ public SkeletonUpdater Skeleton private void CheckSkeleton() { + if (model == null) + return; + if (modelViewHierarchyDirty || meshInfos.Count != model.Meshes.Count) { ModelUpdated(); diff --git a/sources/presentation/Stride.Core.Presentation.Dialogs/DialogService.cs b/sources/presentation/Stride.Core.Presentation.Dialogs/DialogService.cs index 8c43e8b5dc..f67347e8f4 100644 --- a/sources/presentation/Stride.Core.Presentation.Dialogs/DialogService.cs +++ b/sources/presentation/Stride.Core.Presentation.Dialogs/DialogService.cs @@ -20,6 +20,17 @@ namespace Stride.Core.Presentation.Dialogs { public class DialogService : IDialogService2 { + /// + /// When true on the current thread, dialog methods skip showing UI and record messages instead. + /// Used by MCP dispatch to prevent modal dialogs from blocking tool execution. + /// + [ThreadStatic] public static bool SuppressDialogs; + + /// + /// When is true, dialog messages are added here (if non-null). + /// + [ThreadStatic] public static List SuppressedDialogMessages; + private Action onClosedAction; public DialogService([NotNull] IDispatcherService dispatcher, string applicationName) @@ -51,46 +62,91 @@ public IFileSaveModalDialog CreateFileSaveModalDialog() public async Task MessageBoxAsync(string message, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return MessageBoxResult.OK; + } return (MessageBoxResult)await DialogHelper.MessageBox(Dispatcher, message, ApplicationName, IDialogService.GetButtons(buttons), image); } public Task MessageBoxAsync(string message, IReadOnlyCollection buttons, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return Task.FromResult(0); + } return DialogHelper.MessageBox(Dispatcher, message, ApplicationName, buttons, image); } public async Task CheckedMessageBoxAsync(string message, bool? isChecked, string checkboxMessage, MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return new CheckedMessageBoxResult(MessageBoxResult.OK, isChecked); + } return await DialogHelper.CheckedMessageBox(Dispatcher, message, ApplicationName, isChecked, checkboxMessage, IDialogService.GetButtons(button), image); } public Task CheckedMessageBoxAsync(string message, bool? isChecked, string checkboxMessage, IReadOnlyCollection buttons, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return Task.FromResult(new CheckedMessageBoxResult(MessageBoxResult.OK, isChecked)); + } return DialogHelper.CheckedMessageBox(Dispatcher, message, ApplicationName, isChecked, checkboxMessage, buttons, image); } public MessageBoxResult BlockingMessageBox(string message, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return MessageBoxResult.OK; + } return (MessageBoxResult)DialogHelper.BlockingMessageBox(Dispatcher, message, ApplicationName, IDialogService.GetButtons(buttons), image); } public int BlockingMessageBox(string message, IEnumerable buttons, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return 0; + } return DialogHelper.BlockingMessageBox(Dispatcher, message, ApplicationName, buttons, image); } public CheckedMessageBoxResult BlockingCheckedMessageBox(string message, bool? isChecked, MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return new CheckedMessageBoxResult(MessageBoxResult.OK, isChecked); + } return BlockingCheckedMessageBox(message, isChecked, DialogHelper.DontAskAgain, button, image); } public CheckedMessageBoxResult BlockingCheckedMessageBox(string message, bool? isChecked, string checkboxMessage, MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return new CheckedMessageBoxResult(MessageBoxResult.OK, isChecked); + } return DialogHelper.BlockingCheckedMessageBox(Dispatcher, message, ApplicationName, isChecked, checkboxMessage, IDialogService.GetButtons(button), image); } public CheckedMessageBoxResult BlockingCheckedMessageBox(string message, bool? isChecked, string checkboxMessage, IEnumerable buttons, MessageBoxImage image = MessageBoxImage.None) { + if (SuppressDialogs) + { + SuppressedDialogMessages?.Add(message); + return new CheckedMessageBoxResult(MessageBoxResult.OK, isChecked); + } return DialogHelper.BlockingCheckedMessageBox(Dispatcher, message, ApplicationName, isChecked, checkboxMessage, buttons, image); }