Compare commits
206 Commits
feature/as
...
e7fedfd35a
| Author | SHA1 | Date | |
|---|---|---|---|
| e7fedfd35a | |||
| e384a2f38c | |||
| 0eaf7cd51d | |||
| 631638f3fb | |||
| e3a02437c3 | |||
| 5903ddda2b | |||
| 1a91811621 | |||
| 4757c0c91a | |||
| 3533d3367f | |||
| 884611181a | |||
| cb4092179f | |||
| c249a389e3 | |||
| ed00f205b0 | |||
| 4f5556ee1b | |||
| abd5ad74d5 | |||
| 13bf1501e4 | |||
| 6615fe794e | |||
| d9bfa43663 | |||
| 817b32b8d9 | |||
| c66fda5332 | |||
| f9a6e9cbbe | |||
| 4ed5572ce7 | |||
| 68fda03aa9 | |||
| 0fc449bc78 | |||
| a5c10cfe5a | |||
| 6c96d4cf50 | |||
| c6bdbe0710 | |||
| effd33b285 | |||
| 92970f85ef | |||
| 2dc97f3149 | |||
| ba9e24c46c | |||
| 6321b36ef5 | |||
| d03eb659fa | |||
| e32a24739d | |||
| eb41f23582 | |||
| 3157596b5d | |||
| a00cb27529 | |||
| 0b6e5b8501 | |||
| 89e6c68f2a | |||
| b28b32f502 | |||
| fa617accc3 | |||
| ff22b89ba3 | |||
| 2e6e705558 | |||
| e6e38f5eea | |||
| d15bd22743 | |||
| 15870ffe89 | |||
| 70b7e56eb7 | |||
| 257838b33e | |||
| 8ff98c56be | |||
| 2c84696994 | |||
| a33a150d06 | |||
| 60ef684d80 | |||
| 90ac5e6d4b | |||
| bd13e7faa0 | |||
| b5d8009bec | |||
| 3aef53cad9 | |||
| 99adf8fc3b | |||
| 1c553a55fa | |||
| e9f822409d | |||
| 0d8bc6f868 | |||
| c8f24edfd8 | |||
| 2946b905c6 | |||
| 666528263b | |||
| c52daf3914 | |||
| 9738971369 | |||
| af56338347 | |||
| 45711e7770 | |||
| 0f0b36a932 | |||
| e6d0529ef1 | |||
| d367cff79f | |||
| 35731d4ebe | |||
| 8d3c5ecb1f | |||
| 1d48784a1c | |||
| e5aa328576 | |||
| 55eb240de6 | |||
| 45d810e01c | |||
| 1ec8496b8b | |||
| 45375ac2ff | |||
| 4188152f49 | |||
| 5521a8cce2 | |||
| baca976c6f | |||
| b87e01f6b3 | |||
| 2fa9976658 | |||
| e92e365a3a | |||
| 09576bb6e1 | |||
| 332a940993 | |||
| acbf315e8f | |||
| 11101f8352 | |||
| bf40eabcac | |||
| ea4d1084e9 | |||
| 47ffc01524 | |||
| 5f0eea49cf | |||
| 51398f29d2 | |||
| 17588439fa | |||
| 668e66937b | |||
| 5845e7e9fb | |||
| de71043be3 | |||
| 3f6de84387 | |||
| 975c359bf4 | |||
| 71abd60a75 | |||
| 777c4ef31d | |||
| 3c9c95ad73 | |||
| 4713bfe7da | |||
| 9a1b8dcab0 | |||
| ea7d3fad26 | |||
| c77592d479 | |||
| 287b3b303f | |||
| 7ac9a66110 | |||
| 0a0359ec06 | |||
| cda3b292b5 | |||
| 65a335fc1a | |||
| c1f7f3e14e | |||
| a409a93a10 | |||
| 5ceb7c11ed | |||
| e80266f2bc | |||
| 04a3b924ab | |||
| 10bc76a654 | |||
| c4c0b5cd87 | |||
| 08e4d3311a | |||
| 299bcf520c | |||
| 304df0a381 | |||
| 8c136709ff | |||
| e83555498a | |||
| 07274b6699 | |||
| 095fcc87a7 | |||
| 419552439d | |||
| 5efd0c8aee | |||
| b3d753fd08 | |||
| e69e071ce2 | |||
| 231756006e | |||
| 98405cb8ec | |||
| 4aeaecfe81 | |||
| c6a71e599b | |||
| b194b57e4e | |||
| 1cd0971b4d | |||
| c2cfd18273 | |||
| 7d759c8797 | |||
| a2c2198715 | |||
| 8d789af888 | |||
| dee33958b9 | |||
| bb0f9be600 | |||
| 49e6bbe8b0 | |||
| ad90bf1d34 | |||
| c0116d5409 | |||
| 8d49dba2f1 | |||
| ad928feea2 | |||
| 17090eaa0d | |||
| 56b84effb6 | |||
| 944687848e | |||
| 038a13bbe0 | |||
| efc9e8862d | |||
| 979f1d64a7 | |||
| 87217337b7 | |||
| 3ea4260405 | |||
| 4052ffb854 | |||
| 8ba976b0ba | |||
| f38ad04c4f | |||
| dd41cafd64 | |||
| d8a7b07624 | |||
| 0a2eb619eb | |||
| 447a4e6904 | |||
| b729ca86f5 | |||
| 7860e5e341 | |||
| 92e3d33361 | |||
| d44ec0be31 | |||
| 2b3bf21a74 | |||
| 37f4795b4f | |||
| 793df1af4f | |||
| a35321df89 | |||
| db0be367ef | |||
| 4a98e44630 | |||
| 9cf03e0b6f | |||
| bc78c8fbee | |||
| fe49e57330 | |||
| 2376fc9414 | |||
| 0a3502b858 | |||
| 22fdae1061 | |||
| 92c503b253 | |||
| f7fb7da496 | |||
| 2ba60c4bae | |||
| f2b68955b1 | |||
| 85a000e5c4 | |||
| 301a6d1c45 | |||
| e831b71a79 | |||
| 9bae3e647e | |||
| 6cadd8edeb | |||
| 3e4084c42a | |||
| cce1cf7256 | |||
| 254b08bc81 | |||
| 912b320d8f | |||
| 8a3b40b4f8 | |||
| 619720feee | |||
| bfe8588d76 | |||
| b8af6e8c3a | |||
| 5e42d699c3 | |||
| 6f802ac12b | |||
| 162b71f309 | |||
| 30090f84ab | |||
| 93c58fa7fb | |||
| 78e3b4ef31 | |||
| db8ca971a8 | |||
| 638417d4f0 | |||
| 426786397c | |||
| 9bbccfc8f8 | |||
| eadd13931f | |||
| 59991f47d5 |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.dll filter=lfs diff=lfs merge=lfs -text
|
||||
9
.gitignore
vendored
@@ -10,6 +10,15 @@
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
AGENTS.md
|
||||
.opencode/
|
||||
.code-review-graph/
|
||||
.github/instructions/
|
||||
|
||||
ref/
|
||||
docfx/
|
||||
NUL
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
|
||||
297
AGENTS.md
@@ -1,297 +0,0 @@
|
||||
# GhostEngine - Agent Development Guide
|
||||
|
||||
This guide provides essential information for AI coding agents working on the GhostEngine codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
- **Type**: Game Engine
|
||||
- **Language**: C#
|
||||
- **Target Framework**: .NET 10.0
|
||||
- **Special Features**: ECS architecture, D3D12 rendering, AOT compilation, WinUI 3 editor
|
||||
- **Platform**: Windows (net10.0-windows10.0.22621.0 for editor projects)
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Build Entire Solution
|
||||
```bash
|
||||
dotnet build GhostEngine.slnx
|
||||
```
|
||||
|
||||
### Build Specific Project
|
||||
```bash
|
||||
dotnet build Ghost.Entities/Ghost.Entities.csproj
|
||||
dotnet build Ghost.Editor/Ghost.Editor.csproj
|
||||
```
|
||||
|
||||
### Build with Configuration
|
||||
```bash
|
||||
dotnet build GhostEngine.slnx -c Release
|
||||
dotnet build GhostEngine.slnx -c Debug
|
||||
```
|
||||
|
||||
### Clean Build
|
||||
```bash
|
||||
dotnet clean GhostEngine.slnx
|
||||
dotnet build GhostEngine.slnx
|
||||
```
|
||||
|
||||
## Test Commands
|
||||
|
||||
### Run All Tests (Custom Framework)
|
||||
Tests use a custom test framework (not xUnit/NUnit/MSTest). Each test project is an executable.
|
||||
|
||||
```bash
|
||||
# Run entity tests
|
||||
dotnet run --project Ghost.Entities.Test/Ghost.Entities.Test.csproj
|
||||
|
||||
# Run shader tests
|
||||
dotnet run --project Ghost.Shader.Test/Ghost.Shader.Test.csproj
|
||||
```
|
||||
|
||||
### Run Single Test
|
||||
Tests implement `ITest` interface. To run a specific test, modify the test project's `Program.cs`:
|
||||
|
||||
```csharp
|
||||
// In Ghost.Entities.Test/Program.cs
|
||||
TestRunner.Run<EntityQueryTest>(); // Run specific test
|
||||
TestRunner.Run<EntityQueryTest>(10); // Run with 10 iterations
|
||||
```
|
||||
|
||||
### Visual Tests (Graphics)
|
||||
Graphics tests use WinUI 3 and require running as packaged apps:
|
||||
|
||||
```bash
|
||||
dotnet run --project Ghost.Graphics.Test/Ghost.Graphics.Test.csproj
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Formatting (from .editorconfig)
|
||||
|
||||
- **Braces**: Allman style - all opening braces on new lines
|
||||
- **Line Length**: Max 400 characters (very permissive)
|
||||
- **Single-line statements**: Preserved (allowed)
|
||||
- **Single-line blocks**: Preserved (allowed)
|
||||
|
||||
```csharp
|
||||
// Correct brace style
|
||||
public void Method()
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
DoSomething();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
- **System directives**: NOT sorted first (dotnet_sort_system_directives_first = false)
|
||||
- **No grouping**: Import directives not separated by blank lines
|
||||
- **Order**: Organize by project convention, not alphabetically
|
||||
|
||||
```csharp
|
||||
using Ghost.Core;
|
||||
using Ghost.Entities;
|
||||
using Misaki.HighPerformance.Collections;
|
||||
using System.Diagnostics;
|
||||
using TerraFX.Interop.DirectX;
|
||||
```
|
||||
|
||||
### Types and Nullability
|
||||
|
||||
- **Nullable**: Enabled for all projects
|
||||
- **Implicit usings**: Enabled
|
||||
- **Unsafe code**: Allowed in most projects (AllowUnsafeBlocks = True)
|
||||
- **Primary constructors**: NOT preferred (csharp_style_prefer_primary_constructors = false)
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Classes/Interfaces**: PascalCase (`EntityManager`, `ICommandBuffer`)
|
||||
- **Methods**: PascalCase (`CreateEntity`, `GetComponent`)
|
||||
- **Properties**: PascalCase (`IsSuccess`, `Value`)
|
||||
- **Fields (private)**: Camel case with underscore prefix (`_entityLocations`, `_world`)
|
||||
- **Fields (public/internal)**: Camel case, no prefix for struct fields (`archetypeID`, `chunkIndex`)
|
||||
- **Type parameters**: Single letter or PascalCase (`T`, `TComponent`)
|
||||
- **Constants**: PascalCase (no SCREAMING_SNAKE_CASE)
|
||||
|
||||
```csharp
|
||||
public class EntityManager
|
||||
{
|
||||
private readonly World _world; // Private field
|
||||
private UnsafeSlotMap<EntityLocation> _entityLocations;
|
||||
|
||||
public World World => _world; // Property
|
||||
|
||||
public Entity CreateEntity() { } // Method
|
||||
}
|
||||
|
||||
internal struct EntityLocation // Struct
|
||||
{
|
||||
public int archetypeID; // Public struct field
|
||||
public int chunkIndex;
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
**Use Result Types** - Railway-oriented programming pattern:
|
||||
|
||||
```csharp
|
||||
// Custom result types defined in Ghost.Core
|
||||
public ErrorStatus DoOperation()
|
||||
{
|
||||
return ErrorStatus.None; // or ErrorStatus.NotFound, etc.
|
||||
}
|
||||
|
||||
public Result<T> GetValue()
|
||||
{
|
||||
if (success)
|
||||
return Result<T>.Success(value);
|
||||
else
|
||||
return Result<T>.Failure("Error message");
|
||||
}
|
||||
|
||||
public Result<T, ErrorStatus> GetValueWithStatus()
|
||||
{
|
||||
if (success)
|
||||
return value; // Implicit conversion
|
||||
else
|
||||
return ErrorStatus.NotFound; // Implicit conversion
|
||||
}
|
||||
|
||||
// Extension methods for checking results
|
||||
result.ThrowIfFailed();
|
||||
var value = result.GetValueOrThrow();
|
||||
var value = result.GetValueOrDefault(defaultValue);
|
||||
if (result.TryGetValue(out var value)) { }
|
||||
```
|
||||
|
||||
**Error Status Values**: None, NotFound, InvalidArgument, InvalidState, InternalError, PermissionDenied, NotSupported, OutOfMemory, Timeout, Cancelled, UnknownError
|
||||
|
||||
### Memory and Performance
|
||||
|
||||
- **Use unsafe code** when needed for performance-critical paths
|
||||
- **Span<T> and stackalloc**: Prefer for temporary allocations
|
||||
- **ref returns**: Use for zero-copy access to internal data
|
||||
- **Allocator patterns**: Use `Allocator.Persistent` for long-lived allocations
|
||||
- **AllocationManager**: Create stack scopes for temporary allocations
|
||||
|
||||
```csharp
|
||||
// Stack allocation pattern
|
||||
var entities = (Span<Entity>)stackalloc Entity[1];
|
||||
|
||||
// Using allocation scope
|
||||
using var scope = AllocationManager.CreateStackScope();
|
||||
var batchDestroy = new UnsafeList<EntityLocation>(entities.Length, scope.AllocationHandle);
|
||||
|
||||
// Ref returns for zero-copy access
|
||||
public ref T GetSingleton<T>() where T : unmanaged, IComponent
|
||||
{
|
||||
var ptr = GetSingleton(ComponentTypeID<T>.Value);
|
||||
return ref *(T*)ptr;
|
||||
}
|
||||
```
|
||||
|
||||
### Type Safety Patterns
|
||||
|
||||
**Strongly-typed identifiers**:
|
||||
```csharp
|
||||
Identifier<IComponent> componentID;
|
||||
Identifier<Archetype> archetypeID;
|
||||
Handle<T> resourceHandle;
|
||||
```
|
||||
|
||||
**Generic constraints**:
|
||||
```csharp
|
||||
public void Method<T>() where T : unmanaged, IComponent
|
||||
public void Method<T, E>() where E : struct, Enum
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- **XML comments**: Required for public APIs
|
||||
- **Summary tags**: Describe what, not how
|
||||
- **Remarks**: Add for complex behavior, thread-safety warnings, structural changes
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Create an entity with specified components.
|
||||
/// </summary>
|
||||
/// <param name="set">A set of component space IDs to add to the entities.</param>
|
||||
/// <returns>The created entity.</returns>
|
||||
/// <remarks>
|
||||
/// This method causes structural changes and is not thread-safe.
|
||||
/// Use <see cref="EntityCommandBuffer"/> to defer changes.
|
||||
/// </remarks>
|
||||
public Entity CreateEntity(ComponentSet set) { }
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**ECS Component Registration**:
|
||||
```csharp
|
||||
// Type-safe component ID
|
||||
ComponentTypeID<Transform>.Value
|
||||
|
||||
// Component sets for archetypes
|
||||
var set = new ComponentSet(ComponentTypeID<Transform>.Value, ComponentTypeID<Velocity>.Value);
|
||||
```
|
||||
|
||||
**Disposal Pattern**:
|
||||
```csharp
|
||||
private bool _disposed;
|
||||
|
||||
~MyClass()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
// Cleanup code
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
```
|
||||
|
||||
**Debug-only validation**:
|
||||
```csharp
|
||||
#if DEBUG || GHOST_EDITOR
|
||||
if (!_isSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"Error: {_message}");
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Entity Component System (ECS)
|
||||
- Archetype-based storage (similar to Unity DOTS)
|
||||
- Component data stored in chunks
|
||||
- Queries use bitset signatures for fast matching
|
||||
- Structural changes move entities between archetypes
|
||||
|
||||
### Graphics (D3D12)
|
||||
- Hardware abstraction via `ICommandBuffer`
|
||||
- Resource lifetime managed via handles
|
||||
- Pipeline state objects (PSO) cached in library
|
||||
- Native interop via TerraFX.Interop
|
||||
|
||||
### Custom Dependencies
|
||||
- `Misaki.HighPerformance.*`: High-performance collections and utilities
|
||||
- `TerraFX.Interop.*`: Native Windows/DirectX interop
|
||||
- Custom source generators in `Ghost.Generator`
|
||||
|
||||
## Important Rules
|
||||
|
||||
1. **Never disable nullable warnings** - fix the root cause
|
||||
2. **Use Result types** instead of throwing exceptions for expected failures
|
||||
3. **Document thread-safety** in XML comments for public APIs
|
||||
4. **AllowUnsafeBlocks** is enabled - use unsafe code when it improves performance
|
||||
5. **Avoid collection expressions/initializers** (disabled in .editorconfig)
|
||||
6. **Prefer explicit over implicit** - clarity over brevity
|
||||
7. **Test changes** by running the appropriate test project executable
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Ghost.Core.Contracts;
|
||||
|
||||
public interface ICloneable
|
||||
{
|
||||
object Clone();
|
||||
}
|
||||
|
||||
public interface ICloneable<T>
|
||||
{
|
||||
T Clone();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Ghost.Core.Contracts;
|
||||
|
||||
internal interface IReleasable
|
||||
{
|
||||
void InternalRelease();
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
namespace Ghost.Core.Graphics;
|
||||
|
||||
public enum KeywordSpace
|
||||
{
|
||||
Local,
|
||||
Global,
|
||||
}
|
||||
|
||||
public enum ShaderPropertyType
|
||||
{
|
||||
None,
|
||||
Float, Float2, Float3, Float4,
|
||||
Float4x4,
|
||||
Int, Int2, Int3, Int4,
|
||||
UInt, UInt2, UInt3, UInt4,
|
||||
Bool, Bool2, Bool3, Bool4,
|
||||
Texture2D, Texture3D, TextureCube,
|
||||
Texture2DArray, TextureCubeArray,
|
||||
Sampler
|
||||
}
|
||||
|
||||
public struct ShaderEntryPoint
|
||||
{
|
||||
public string entry;
|
||||
public string shader;
|
||||
|
||||
public readonly bool IsCreated => !string.IsNullOrEmpty(entry) && !string.IsNullOrEmpty(shader);
|
||||
}
|
||||
|
||||
public struct KeywordsGroup
|
||||
{
|
||||
public KeywordSpace space;
|
||||
public List<string> keywords;
|
||||
}
|
||||
|
||||
public struct PropertyDescriptor
|
||||
{
|
||||
public ShaderPropertyType type;
|
||||
public string name;
|
||||
public object? defaultValue;
|
||||
}
|
||||
|
||||
public struct PassDescriptor
|
||||
{
|
||||
public string identifier;
|
||||
public string name;
|
||||
|
||||
public ShaderEntryPoint taskShader;
|
||||
public ShaderEntryPoint meshShader;
|
||||
public ShaderEntryPoint pixelShader;
|
||||
public string[] defines;
|
||||
public string[] includes;
|
||||
public KeywordsGroup[] keywords;
|
||||
public PipelineState localPipeline;
|
||||
public string? hlsl;
|
||||
}
|
||||
|
||||
public class ShaderDescriptor
|
||||
{
|
||||
public string name = string.Empty;
|
||||
public uint cbufferSize;
|
||||
public PropertyDescriptor[] globalProperties = null!;
|
||||
public PropertyDescriptor[] properties = null!;
|
||||
public PassDescriptor[] passes = null!;
|
||||
public string? hlsl;
|
||||
}
|
||||
|
||||
public static class ShaderDescriptorExtensions
|
||||
{
|
||||
public static uint GetSize(this ShaderPropertyType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ShaderPropertyType.Float => 4,
|
||||
ShaderPropertyType.Float2 => 8,
|
||||
ShaderPropertyType.Float3 => 12,
|
||||
ShaderPropertyType.Float4 => 16,
|
||||
ShaderPropertyType.Float4x4 => 64,
|
||||
ShaderPropertyType.Int => 4,
|
||||
ShaderPropertyType.Int2 => 8,
|
||||
ShaderPropertyType.Int3 => 12,
|
||||
ShaderPropertyType.Int4 => 16,
|
||||
ShaderPropertyType.UInt => 4,
|
||||
ShaderPropertyType.UInt2 => 8,
|
||||
ShaderPropertyType.UInt3 => 12,
|
||||
ShaderPropertyType.UInt4 => 16,
|
||||
ShaderPropertyType.Bool => 4,
|
||||
ShaderPropertyType.Bool2 => 8,
|
||||
ShaderPropertyType.Bool3 => 12,
|
||||
ShaderPropertyType.Bool4 => 16,
|
||||
ShaderPropertyType.Texture2D => 4, // Bindless resource use uint32
|
||||
ShaderPropertyType.Texture3D => 4,
|
||||
ShaderPropertyType.TextureCube => 4,
|
||||
ShaderPropertyType.Texture2DArray => 4,
|
||||
ShaderPropertyType.TextureCubeArray => 4,
|
||||
ShaderPropertyType.Sampler => 4,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ghost.Core;
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public readonly struct LogMessage
|
||||
{
|
||||
public LogLevel Level
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string Message
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string? StackTrace
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public DateTime Timestamp
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public LogMessage(LogLevel level, string message, string? stackTrace = null)
|
||||
{
|
||||
Level = level;
|
||||
Message = message;
|
||||
StackTrace = stackTrace;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (StackTrace != null)
|
||||
{
|
||||
return $"{Timestamp:HH:mm:ss} [{Level}] {Message}\n{StackTrace}";
|
||||
}
|
||||
|
||||
return $"{Timestamp:HH:mm:ss} [{Level}] {Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILogger
|
||||
{
|
||||
ReadOnlyObservableCollection<LogMessage> Logs
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
void Log(string message, LogLevel level);
|
||||
void Log(Exception exception);
|
||||
void Assert(bool condition, string message);
|
||||
void Clear();
|
||||
}
|
||||
|
||||
public static class Logger
|
||||
{
|
||||
// TODO: Add file logging.
|
||||
private class LoggerImpl : ILogger
|
||||
{
|
||||
private readonly ObservableCollection<LogMessage> _logs = new();
|
||||
private readonly ReadOnlyObservableCollection<LogMessage> _readOnly;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public ReadOnlyObservableCollection<LogMessage> Logs => _readOnly;
|
||||
|
||||
public LoggerImpl()
|
||||
{
|
||||
_readOnly = new ReadOnlyObservableCollection<LogMessage>(_logs);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public void Log(string message, LogLevel level)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_logs.Add(new LogMessage(level, message));
|
||||
}
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public void Log(Exception exception)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_logs.Add(new LogMessage(LogLevel.Error, exception.Message, exception.StackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public void Assert(bool condition, string message)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
Log(message, LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_logs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ILogger s_logger = new LoggerImpl();
|
||||
|
||||
public static ReadOnlyObservableCollection<LogMessage> Logs => s_logger.Logs;
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void Log(LogLevel level, object? message)
|
||||
{
|
||||
s_logger.Log(message?.ToString() ?? "null", level);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void Log(LogLevel level, string message)
|
||||
{
|
||||
s_logger.Log(message, level);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void Log(LogLevel level, string format, params object?[] args)
|
||||
{
|
||||
s_logger.Log(string.Format(format, args), level);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogInfo(object? message)
|
||||
{
|
||||
s_logger.Log(message?.ToString() ?? "null", LogLevel.Info);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogInfo(string message)
|
||||
{
|
||||
s_logger.Log(message, LogLevel.Info);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogInfo(string format, params object?[] args)
|
||||
{
|
||||
s_logger.Log(string.Format(format, args), LogLevel.Info);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogWarning(object? message)
|
||||
{
|
||||
s_logger.Log(message?.ToString() ?? "null", LogLevel.Warning);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogWarning(string message)
|
||||
{
|
||||
s_logger.Log(message, LogLevel.Warning);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogWarning(string format, params object?[] args)
|
||||
{
|
||||
s_logger.Log(string.Format(format, args), LogLevel.Warning);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogError(object? message)
|
||||
{
|
||||
s_logger.Log(message?.ToString() ?? "null", LogLevel.Error);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogError(string message)
|
||||
{
|
||||
s_logger.Log(message, LogLevel.Error);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogError(string format, params object?[] args)
|
||||
{
|
||||
s_logger.Log(string.Format(format, args), LogLevel.Error);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void LogError(Exception ex)
|
||||
{
|
||||
s_logger.Log(ex);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void Assert(bool condition, string message)
|
||||
{
|
||||
s_logger.Assert(condition, message);
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
s_logger.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Core;
|
||||
|
||||
public readonly struct TypeHandle
|
||||
{
|
||||
public readonly IntPtr Value
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
private TypeHandle(IntPtr value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the space handle for the specified space.
|
||||
/// </summary>
|
||||
/// <param name="type">The space to get the handle for.</param>
|
||||
/// <returns>The space handle as a nint.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TypeHandle Get(Type type) => new TypeHandle(type.TypeHandle.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the space handle for the specified space.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The space to get the handle for.</typeparam>
|
||||
/// <returns>The space handle as a nint.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TypeHandle Get<T>() => Get(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// Converts a TypeHandle to a Type.
|
||||
/// </summary>
|
||||
/// <param name="handle">The TypeHandle to convert.</param>
|
||||
/// <returns>The corresponding Type.</returns>
|
||||
public Type? ToType()
|
||||
{
|
||||
return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(Value));
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
|
||||
public static implicit operator TypeHandle(IntPtr value)
|
||||
{
|
||||
return new TypeHandle(value);
|
||||
}
|
||||
|
||||
public static implicit operator IntPtr(TypeHandle handle)
|
||||
{
|
||||
return handle.Value;
|
||||
}
|
||||
|
||||
public static implicit operator TypeHandle(Type type)
|
||||
{
|
||||
return Get(type);
|
||||
}
|
||||
|
||||
public static implicit operator Type?(TypeHandle handle)
|
||||
{
|
||||
return handle.ToType();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ghost.Core.Utilities;
|
||||
|
||||
internal class EnumUtility
|
||||
{
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Ghost.Core.Contracts;
|
||||
|
||||
namespace Ghost.Core.Utilities;
|
||||
|
||||
internal static class InternalResource
|
||||
{
|
||||
public static void Release<T>(ref T? resource)
|
||||
where T : IReleasable
|
||||
{
|
||||
resource?.InternalRelease();
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
using Misaki.HighPerformance.LowLevel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Versioning;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Ghost.Core.Utilities;
|
||||
|
||||
[SupportedOSPlatform("windows10.0.19041.0")]
|
||||
internal static unsafe partial class Win32Utility
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public readonly ref struct IID_PPV
|
||||
{
|
||||
public readonly Guid* iid;
|
||||
public readonly void** ppv;
|
||||
|
||||
public IID_PPV(Guid* iid, void** ppv)
|
||||
{
|
||||
this.iid = iid;
|
||||
this.ppv = ppv;
|
||||
}
|
||||
|
||||
public void Deconstruct(out Guid* iid, out void** ppv)
|
||||
{
|
||||
iid = this.iid;
|
||||
ppv = this.ppv;
|
||||
}
|
||||
}
|
||||
|
||||
public static Guid* IID_NULL
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IID.IID_NULL));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IID_PPV IID_PPV_ARGS<T>(ComPtr<T>* comPtr)
|
||||
where T : unmanaged, IUnknown.Interface
|
||||
{
|
||||
return new IID_PPV(Windows.__uuidof<T>(), (void**)comPtr);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Attach<T>(ref this UniquePtr<T> uPtr, T* other)
|
||||
where T : unmanaged, IUnknown.Interface
|
||||
{
|
||||
var ptr = uPtr.Get();
|
||||
if (ptr != null)
|
||||
{
|
||||
var refCount = ptr->Release();
|
||||
Debug.Assert((refCount != 0) || (ptr != other));
|
||||
}
|
||||
|
||||
uPtr = new UniquePtr<T>(other);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Dispose<T>(ref this UniquePtr<T> uPtr)
|
||||
where T : unmanaged, IUnknown.Interface
|
||||
{
|
||||
var ptr = uPtr.Detach();
|
||||
if (ptr != null)
|
||||
{
|
||||
ptr->Release();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Result ToResult(this HRESULT hr, [CallerArgumentExpression(nameof(hr))] string? op = null)
|
||||
{
|
||||
if (hr.SUCCEEDED)
|
||||
{
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
return Result.Failure($"{op} failed with code {hr}");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void** ReleaseAndGetVoidAddressOf<T>(ref this ComPtr<T> comPtr)
|
||||
where T : unmanaged, IUnknown.Interface
|
||||
{
|
||||
return (void**)comPtr.ReleaseAndGetAddressOf();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ComPtr<T> Move<T>(ref this ComPtr<T> comPtr)
|
||||
where T : unmanaged, IUnknown.Interface
|
||||
{
|
||||
var copy = default(ComPtr<T>);
|
||||
comPtr.Swap(ref copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool HasFlag<T>(this uint flags, T flag)
|
||||
where T : Enum
|
||||
{
|
||||
return (flags & Unsafe.As<T, uint>(ref flag)) != 0;
|
||||
}
|
||||
|
||||
extension(MemoryLeakException)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ThrowIfRefCountNonZero(uint count)
|
||||
{
|
||||
if (count != 0)
|
||||
{
|
||||
throw new MemoryLeakException($"Reference count is not zero: {count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Core.Graphics;
|
||||
using Ghost.DSL.ShaderParser;
|
||||
using System.Text;
|
||||
|
||||
namespace Ghost.DSL.ShaderCompiler;
|
||||
|
||||
public struct DSLShaderError
|
||||
{
|
||||
public string message;
|
||||
public int line;
|
||||
public int column;
|
||||
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"Error at {line}:{column} - {message}";
|
||||
}
|
||||
}
|
||||
|
||||
internal static class DSLShaderCompiler
|
||||
{
|
||||
private const string _GLOBAL_PROPERTY_FILE_NAME = "GlobalData.g.hlsl";
|
||||
private const string _GENERATED_FILE_HEADER = "// Auto-generated shader file. Please do not edit this file directly.";
|
||||
|
||||
private static string GetPassUniqueId(DSLShaderSemantics shader, PassSemantic pass)
|
||||
{
|
||||
return $"{shader.name}_{pass.name}";
|
||||
}
|
||||
|
||||
private static PipelineState MeragePipeline(PipelineSemantic? semantic, PipelineState parent)
|
||||
{
|
||||
if (semantic == null)
|
||||
{
|
||||
return parent;
|
||||
}
|
||||
|
||||
return new PipelineState
|
||||
{
|
||||
ZTest = semantic.zTest ?? parent.ZTest,
|
||||
ZWrite = semantic.zWrite ?? parent.ZWrite,
|
||||
Cull = semantic.cull ?? parent.Cull,
|
||||
Blend = semantic.blend ?? parent.Blend,
|
||||
ColorMask = semantic.colorMask ?? parent.ColorMask
|
||||
};
|
||||
}
|
||||
|
||||
private static uint CalculateCBufferSize(ReadOnlySpan<PropertyDescriptor> properties)
|
||||
{
|
||||
if (properties.IsEmpty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var currentOffset = 0u;
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var size = prop.type.GetSize();
|
||||
|
||||
if ((currentOffset % 16) + size > 16)
|
||||
{
|
||||
currentOffset = (currentOffset + 15u) & ~15u;
|
||||
}
|
||||
|
||||
currentOffset += size;
|
||||
}
|
||||
|
||||
return (currentOffset + 15u) & ~15u;
|
||||
}
|
||||
|
||||
// TODO: Implement shader inheritance resolution, including property and pass merging.
|
||||
// Currently, we just ignore inheritance.
|
||||
public static ShaderDescriptor ResolveShader(DSLShaderSemantics semantics)
|
||||
{
|
||||
var descriptor = new ShaderDescriptor
|
||||
{
|
||||
name = semantics.name,
|
||||
hlsl = semantics.hlsl
|
||||
};
|
||||
|
||||
var shaderGlobalProperties = semantics.properties?
|
||||
.Where(p => p.scope == PropertyScope.Global)
|
||||
.Select(p => new PropertyDescriptor
|
||||
{
|
||||
name = p.name,
|
||||
type = p.type,
|
||||
defaultValue = p.defaultValue
|
||||
}).ToArray();
|
||||
|
||||
var shaderLocalProperties = semantics.properties?
|
||||
.Where(p => p.scope == PropertyScope.Local)
|
||||
.Select(p => new PropertyDescriptor
|
||||
{
|
||||
name = p.name,
|
||||
type = p.type,
|
||||
defaultValue = p.defaultValue
|
||||
}).ToArray();
|
||||
|
||||
descriptor.globalProperties = shaderGlobalProperties ?? Array.Empty<PropertyDescriptor>();
|
||||
descriptor.properties = shaderLocalProperties ?? Array.Empty<PropertyDescriptor>();
|
||||
descriptor.cbufferSize = CalculateCBufferSize(descriptor.properties);
|
||||
|
||||
if (semantics.passes != null)
|
||||
{
|
||||
descriptor.passes = new PassDescriptor[semantics.passes.Count];
|
||||
for (int i = 0; i < semantics.passes.Count; i++)
|
||||
{
|
||||
var pass = semantics.passes[i];
|
||||
var localPipeline = MeragePipeline(pass.localPipeline, PipelineState.Default);
|
||||
descriptor.passes[i] = new PassDescriptor
|
||||
{
|
||||
identifier = GetPassUniqueId(semantics, pass),
|
||||
name = pass.name,
|
||||
taskShader = pass.taskShader,
|
||||
meshShader = pass.meshShader,
|
||||
pixelShader = pass.pixelShader,
|
||||
localPipeline = localPipeline,
|
||||
defines = pass.defines?.ToArray() ?? Array.Empty<string>(),
|
||||
includes = pass.includes?.ToArray() ?? Array.Empty<string>(),
|
||||
keywords = pass.keywords?.ToArray() ?? Array.Empty<KeywordsGroup>(),
|
||||
hlsl = pass.hlsl
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
descriptor.passes = Array.Empty<PassDescriptor>();
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static Result<ShaderDescriptor> CompileShader(string shaderPath, string generatedOutputDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
var source = File.ReadAllText(shaderPath);
|
||||
|
||||
// Use ANTLR4 parser
|
||||
var shaderModels = AntlrShaderCompiler.ParseShaders(source, out var parseErrors);
|
||||
|
||||
if (parseErrors.Count != 0)
|
||||
{
|
||||
var errorMessages = new StringBuilder();
|
||||
foreach (var error in parseErrors)
|
||||
{
|
||||
errorMessages.AppendLine(error.ToString());
|
||||
}
|
||||
|
||||
return Result.Failure("Failed to parse shader due to errors:\n" + errorMessages.ToString());
|
||||
}
|
||||
|
||||
if (shaderModels.Count == 0)
|
||||
{
|
||||
return Result.Failure("No shader found in the provided file.");
|
||||
}
|
||||
|
||||
// Convert to semantics
|
||||
var model = AntlrShaderCompiler.ConvertToSemantics(shaderModels[0], out var errors);
|
||||
|
||||
if (errors.Count != 0 || model == null)
|
||||
{
|
||||
var errorMessages = new StringBuilder();
|
||||
foreach (var error in errors)
|
||||
{
|
||||
errorMessages.AppendLine(error.ToString());
|
||||
}
|
||||
|
||||
return Result.Failure("Failed to compile shader due to errors:\n" + errorMessages.ToString());
|
||||
}
|
||||
|
||||
var desc = ResolveShader(model);
|
||||
var globalPropResult = GenerateGlobalProperties(desc.globalProperties, generatedOutputDirectory);
|
||||
if (globalPropResult.IsFailure)
|
||||
{
|
||||
return Result.Failure("Failed to generate global properties: " + globalPropResult.Message);
|
||||
}
|
||||
|
||||
var generatedResult = GenerateShaderCode(desc, generatedOutputDirectory);
|
||||
if (generatedResult.IsFailure)
|
||||
{
|
||||
return Result.Failure("Failed to generate pass files: " + generatedResult.Message);
|
||||
}
|
||||
|
||||
foreach (ref var pass in desc.passes.AsSpan())
|
||||
{
|
||||
if (pass.includes == null)
|
||||
{
|
||||
pass.includes = new string[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Resize(ref pass.includes, pass.includes.Length + 2);
|
||||
// Shift existing includes to make room for the two new includes at the front.
|
||||
pass.includes.AsSpan(0, pass.includes.Length - 2).CopyTo(pass.includes.AsSpan(2));
|
||||
}
|
||||
|
||||
pass.includes[0] = globalPropResult.Value;
|
||||
pass.includes[1] = generatedResult.Value;
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure("Failed to compile shader: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ShaderPropertyTypeToHLSLType(ShaderPropertyType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ShaderPropertyType.Float => "float",
|
||||
ShaderPropertyType.Float2 => "float2",
|
||||
ShaderPropertyType.Float3 => "float3",
|
||||
ShaderPropertyType.Float4 => "float4",
|
||||
ShaderPropertyType.Int => "int",
|
||||
ShaderPropertyType.Int2 => "int2",
|
||||
ShaderPropertyType.Int3 => "int3",
|
||||
ShaderPropertyType.Int4 => "int4",
|
||||
ShaderPropertyType.UInt => "uint",
|
||||
ShaderPropertyType.UInt2 => "uint2",
|
||||
ShaderPropertyType.UInt3 => "uint3",
|
||||
ShaderPropertyType.UInt4 => "uint4",
|
||||
ShaderPropertyType.Bool => "bool",
|
||||
ShaderPropertyType.Bool2 => "bool2",
|
||||
ShaderPropertyType.Bool3 => "bool3",
|
||||
ShaderPropertyType.Bool4 => "bool4",
|
||||
// NOTE: Textures here are bindless, represented as uint (descriptor index).
|
||||
ShaderPropertyType.Texture2D => "TEXTURE2D",
|
||||
ShaderPropertyType.Texture3D => "TEXTURE3D",
|
||||
ShaderPropertyType.TextureCube => "TEXTURECUBE",
|
||||
ShaderPropertyType.Texture2DArray => "TEXTURE2D_ARRAY",
|
||||
ShaderPropertyType.TextureCubeArray => "TEXTURECUBE_ARRAY",
|
||||
ShaderPropertyType.Sampler => "SAMPLER",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported shader property type: {type}")
|
||||
};
|
||||
}
|
||||
|
||||
public static Result<string> GenerateShaderCode(ShaderDescriptor descriptor, string targetDirectory)
|
||||
{
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
return Result.Failure("Target directory does not exist.");
|
||||
}
|
||||
|
||||
var outputFileName = descriptor.name.Replace('/', '_');
|
||||
var outputFilePath = Path.Combine(targetDirectory, outputFileName + ".g.hlsl");
|
||||
var outputDirectory = Path.GetDirectoryName(outputFilePath);
|
||||
|
||||
if (!Directory.Exists(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory!);
|
||||
}
|
||||
|
||||
using var fileStream = File.CreateText(outputFilePath);
|
||||
var fileDefine = outputFileName.Replace('/', '_').ToUpperInvariant() + "_G_HLSL";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine(_GENERATED_FILE_HEADER);
|
||||
sb.AppendLine(@$"
|
||||
#ifndef {fileDefine}
|
||||
#define {fileDefine}
|
||||
|
||||
#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl""");
|
||||
|
||||
sb.Append(@"
|
||||
struct PerMaterialData
|
||||
{");
|
||||
foreach (var prop in descriptor.properties)
|
||||
{
|
||||
sb.Append($@"
|
||||
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
|
||||
}
|
||||
sb.Append(@"
|
||||
};");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(@$"
|
||||
#endif // {fileDefine}");
|
||||
|
||||
fileStream.Write(sb.ToString());
|
||||
|
||||
return outputFilePath;
|
||||
}
|
||||
|
||||
public static Result<string> GenerateGlobalProperties(ReadOnlySpan<PropertyDescriptor> globalProperties, string targetDirectory)
|
||||
{
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
return Result.Failure("Target directory does not exist.");
|
||||
}
|
||||
|
||||
var globalFilePath = Path.Combine(targetDirectory, _GLOBAL_PROPERTY_FILE_NAME);
|
||||
using var globalFileStream = File.CreateText(globalFilePath);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine(_GENERATED_FILE_HEADER);
|
||||
sb.Append(@"
|
||||
#ifndef GLOBALDATA_G_HLSL
|
||||
#define GLOBALDATA_G_HLSL
|
||||
|
||||
#include ""F:/csharp/GhostEngine/Ghost.Graphics/Shaders/Includes/Common.hlsl""
|
||||
|
||||
struct GlobalData
|
||||
{");
|
||||
foreach (var prop in globalProperties)
|
||||
{
|
||||
sb.Append($@"
|
||||
{ShaderPropertyTypeToHLSLType(prop.type)} {prop.name};");
|
||||
}
|
||||
sb.AppendLine(@"
|
||||
};
|
||||
|
||||
#endif // GLOBALDATA_G_HLSL");
|
||||
globalFileStream.Write(sb.ToString());
|
||||
|
||||
return globalFilePath;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Ghost.Data.Repository;
|
||||
|
||||
internal class AssetsRepository
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
using Ghost.Core;
|
||||
|
||||
namespace Ghost.Editor.Core.AppState;
|
||||
|
||||
internal partial class AppStateMachine : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private Dictionary<StateKey, Lazy<IAppState>> _states = new();
|
||||
private IAppState? _current;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void RegisterState(StateKey key, Func<IAppState> stateFactory)
|
||||
{
|
||||
_states[key] = new(stateFactory);
|
||||
}
|
||||
|
||||
public async Task<Result> TransitionToAsync(StateKey stateKey, object? parameter = null)
|
||||
{
|
||||
var previous = _current;
|
||||
if (!_states.TryGetValue(stateKey, out var next))
|
||||
{
|
||||
return Result.Failure($"State '{stateKey}' not found.");
|
||||
}
|
||||
|
||||
Result result;
|
||||
if (previous != null)
|
||||
{
|
||||
result = await previous.OnExitingAsync();
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result = await next.Value.OnEnteringAsync(parameter);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
if (previous != null)
|
||||
{
|
||||
await previous.OnEnteredAsync(parameter);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
result = await previous.OnExitedAsync();
|
||||
if (result.IsFailure)
|
||||
{
|
||||
await next.Value.OnExitedAsync();
|
||||
await previous.OnEnteredAsync(parameter);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result = await next.Value.OnEnteredAsync(parameter);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
await next.Value.OnExitedAsync();
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
await previous.OnEnteredAsync(parameter);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_current = next.Value;
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAsync().AsTask().Wait();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_states.Clear();
|
||||
if (_current != null)
|
||||
{
|
||||
await _current.OnExitingAsync();
|
||||
await _current.OnExitedAsync();
|
||||
}
|
||||
|
||||
_current = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Ghost.Core;
|
||||
|
||||
namespace Ghost.Editor.Core.AppState;
|
||||
|
||||
internal interface IAppState
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when exiting the state.
|
||||
/// </summary>
|
||||
public ValueTask<Result> OnExitingAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Called when entering the state, right after OnEnteringAsync.
|
||||
/// <paramref name="parameter">can be used to pass data into the state, such as a project to load.</summary>
|
||||
/// </summary>
|
||||
public ValueTask<Result> OnEnteringAsync(object? parameter);
|
||||
|
||||
/// <summary>
|
||||
/// Called when exiting the state, specifically for pose transitions.
|
||||
/// </summary>
|
||||
public ValueTask<Result> OnExitedAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Called when entered the state, specifically after the state has been fully initialized and is ready for interaction.
|
||||
/// </summary>
|
||||
/// <param name="parameter">can be used to pass data into the state, such as a project to load.</param>
|
||||
public ValueTask<Result> OnEnteredAsync(object? parameter);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Ghost.Editor.Core.AppState;
|
||||
|
||||
internal enum StateKey
|
||||
{
|
||||
None,
|
||||
Landing,
|
||||
EngineEditor,
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
# Asset Database Plan
|
||||
|
||||
AssetDB is a core component of the Ghost Editor that manages the storage, retrieval, and organization of various assets used within the editor.
|
||||
This document outlines the plan for implementing the AssetDB, including its structure, functionality, and integration with other components of the Ghost Editor.
|
||||
|
||||
## Data Structure
|
||||
|
||||
- Asset Metadata: Each asset will have associated metadata, including:
|
||||
- Unique Identifier (GUID)
|
||||
- Version (Version of the asset pipeline, not the asset. This is primarily for migration when we redesign the asset pipeline in the future)
|
||||
- Tags
|
||||
- Importer Settings
|
||||
|
||||
An simplified example of metadata file (filename.png.gmeta):
|
||||
```json
|
||||
{
|
||||
"Guid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"Version": 1,
|
||||
"Tags": ["Environment", "Texture"],
|
||||
"ImporterSettings": [
|
||||
"TextureImporter": {
|
||||
"Version": 1,
|
||||
"MaxSize": 2048,
|
||||
"MipLevels": 1
|
||||
},
|
||||
"OtherImporter": {
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Asset: The base class for all assets.
|
||||
|
||||
- Asset Database: A centralized database that stores and manages all assets. It will handle:
|
||||
- Asset registration and deregistration
|
||||
- Asset lookup by GUID
|
||||
- Asset lookup by path
|
||||
- Automatic handle file creation, remove, rename, move, etc.
|
||||
- Asset dependency management
|
||||
- Automatic asset re-importing when source files change.
|
||||
- Asset tagging.
|
||||
- Add type specific default importer settings for new asset.
|
||||
- SQLite (`Microsoft.Data.Sqlite`) for persistent storage and efficient querying.
|
||||
|
||||
An simplified data model example in SQLite:
|
||||
```sql
|
||||
CREATE TABLE Assets (
|
||||
Guid TEXT PRIMARY KEY,
|
||||
Path TEXT NOT NULL,
|
||||
Type INTEGER,
|
||||
Version INTEGER,
|
||||
Tags TEXT,
|
||||
DependencyGuids TEXT
|
||||
);
|
||||
```
|
||||
|
||||
## Simplified Workflow
|
||||
|
||||
### New Asset Addition
|
||||
|
||||
1. A file is added to the project directory.
|
||||
2. Generates the metadata for the asset with name filename.etx + ".gmeta" (You can get the extension from Ghost.Editor.Core.Utilities.FileExtensions.META_FILE_EXTENSION)
|
||||
3. Add the asset to the database.
|
||||
|
||||
### File Removal
|
||||
|
||||
1. A file is removed from the project directory.
|
||||
2. Deletes the corresponding asset metadata.
|
||||
3. Remove the asset from the database.
|
||||
4. Mark dependent assets as dirty for re-importing.
|
||||
|
||||
### File Renaming/Moving
|
||||
|
||||
1. A file is renamed or moved within the project directory.
|
||||
2. Check if the new path has an existing metadata file (just in case user move the file with the metadata file together).
|
||||
- If exists, validate the data and update the database accordingly.
|
||||
- if not, regenerate the metadata file for the new path and update the database.
|
||||
3. Delete the old metadata file if exists.
|
||||
|
||||
### File Modification
|
||||
|
||||
1. A file is modified in the project directory.
|
||||
2. Check the file hash to see if it has changed.
|
||||
- If changed, mark the asset as dirty for re-importing.
|
||||
- If not, do nothing.
|
||||
|
||||
### Asset Importing (You don't need to write any assets importer right now, just write the framework and a simple test importer if it's needed for unit test)
|
||||
|
||||
1. An asset is marked as dirty.
|
||||
2. The asset importer for that type processes the asset based on its importer settings.
|
||||
3. Validate the references and dependencies, report errors if any (for example, missing dependencies).
|
||||
|
||||
## Features Checklist
|
||||
|
||||
### In Code (API)
|
||||
|
||||
- [ ] Find GUID by path
|
||||
- [ ] Find path by GUID
|
||||
- [ ] Load asset by GUID (May need special asset loader, not included in this plan, leave an API and TODO comment)
|
||||
- [ ] API for adding/removing/moving/copying assets
|
||||
- [ ] API for opening asset in editor or external program
|
||||
- [ ] API to set asset dirty and save all assets if dirty
|
||||
- [ ] Get and set asset tags.
|
||||
- [ ] Refresh asset database (re-scan project directory for changes)
|
||||
|
||||
### In Editor (API Only, I will handle the UI part)
|
||||
|
||||
- [ ] Asset Browser window to view and manage assets (automatically refresh when assets change)
|
||||
- [ ] Skip meta file in asset browser view
|
||||
- [ ] Search assets by name, tag, type, etc.
|
||||
|
||||
### In Background
|
||||
|
||||
- [ ] File system watcher to monitor changes in the project directory and update the AssetDB accordingly.
|
||||
- [ ] Automatic asset re-importing when source files change (detect changes via file system watcher and quick hash comparison).
|
||||
- [ ] Asset dependency management.
|
||||
- [ ] Validate and fix AssetDB on project load (check for missing/corrupted assets if user add/delete/rename/move files when the editor is not running, etc.)
|
||||
- [ ] Asset importer system to handle different asset types and their import settings.
|
||||
|
||||
## Testing
|
||||
|
||||
Make sure everything builds correctly at first.
|
||||
Write unit tests and integration tests to ensure the AssetDB functions correctly inside the `Ghost.UnitTests` project.
|
||||
|
||||
## Critical Considerations
|
||||
|
||||
- Performance: Ensure that the AssetDB operations are efficient, especially for large projects with many assets.
|
||||
- Stability: The meta data files should be the only source of truth for asset information.
|
||||
The AssetDB should be able to recover from inconsistencies by re-generating data from the meta files.
|
||||
We still need to trust the meta data files even if they are corrupted or missing by regenerating them when necessary.
|
||||
Database is used for caching and quick lookup only.
|
||||
- Asynchronous patterns: Consider using asynchronous operations for file I/O and database operations to avoid blocking the main thread.
|
||||
Packages like `Microsoft.Data.Sqlite` support async operations.
|
||||
@@ -1,355 +0,0 @@
|
||||
using Ghost.Core;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new asset at the specified path.
|
||||
/// Generates metadata and adds it to the database.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Path to create the asset at.</param>
|
||||
/// <param name="content">Content to write to the asset file.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static async ValueTask<Result> CreateAssetAsync(string assetPath, ReadOnlyMemory<byte> content, CancellationToken token = default)
|
||||
{
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
return Result.Failure("AssetsDirectory not initialized");
|
||||
}
|
||||
|
||||
if (!assetPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Result.Failure("Asset path must be within the Assets directory");
|
||||
}
|
||||
|
||||
if (File.Exists(assetPath))
|
||||
{
|
||||
return Result.Failure("Asset already exists");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(assetPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
using var fs = File.Create(assetPath);
|
||||
await fs.WriteAsync(content, token);
|
||||
|
||||
// GenerateMetaFileAsync will be called automatically by the file watcher
|
||||
// But we'll call it directly to ensure it's created immediately
|
||||
await GenerateMetaFileAsync(assetPath, token);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to create asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an empty asset at the specified path.
|
||||
/// Generates metadata and adds it to the database.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Path to create the asset at.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static ValueTask<Result> CreateAssetAsync(string assetPath, CancellationToken token = default)
|
||||
{
|
||||
return CreateAssetAsync(assetPath, ReadOnlyMemory<byte>.Empty, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an asset and its metadata.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset to delete.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static async ValueTask<Result> DeleteAssetAsync(Guid guid, CancellationToken token = default)
|
||||
{
|
||||
var pathResult = GuidToPath(guid);
|
||||
if (pathResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(pathResult.Message);
|
||||
}
|
||||
|
||||
var fullPathResult = GetFullPath(pathResult.Value);
|
||||
if (fullPathResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(fullPathResult.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assetPath = fullPathResult.Value;
|
||||
|
||||
// Delete the asset file
|
||||
if (File.Exists(assetPath))
|
||||
{
|
||||
File.Delete(assetPath);
|
||||
}
|
||||
|
||||
// Delete the .gmeta file
|
||||
var metaPath = assetPath + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
if (File.Exists(metaPath))
|
||||
{
|
||||
File.Delete(metaPath);
|
||||
}
|
||||
|
||||
// Remove from database
|
||||
await RemoveAssetFromDatabaseAsync(guid, token);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to delete asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an asset and its metadata by path.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Path to the asset to delete.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static ValueTask<Result> DeleteAssetAsync(string assetPath, CancellationToken token = default)
|
||||
{
|
||||
var guidResult = PathToGuid(assetPath);
|
||||
if (guidResult.IsFailure)
|
||||
{
|
||||
return new ValueTask<Result>(Task.FromResult(Result.Failure(guidResult.Message)));
|
||||
}
|
||||
|
||||
return DeleteAssetAsync(guidResult.Value, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move an asset to a new location.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset to move.</param>
|
||||
/// <param name="newPath">New path for the asset (relative or absolute).</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static async ValueTask<Result> MoveAssetAsync(Guid guid, string newPath, CancellationToken token = default)
|
||||
{
|
||||
var oldPathResult = GuidToPath(guid);
|
||||
if (oldPathResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(oldPathResult.Message);
|
||||
}
|
||||
|
||||
var oldFullPathResult = GetFullPath(oldPathResult.Value);
|
||||
if (oldFullPathResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(oldFullPathResult.Message);
|
||||
}
|
||||
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
return Result.Failure("AssetsDirectory not initialized");
|
||||
}
|
||||
|
||||
// Ensure new path is absolute and within assets directory
|
||||
if (!Path.IsPathRooted(newPath))
|
||||
{
|
||||
newPath = Path.Combine(AssetsDirectory.FullName, newPath);
|
||||
}
|
||||
|
||||
if (!newPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Result.Failure("New path must be within the Assets directory");
|
||||
}
|
||||
|
||||
if (File.Exists(newPath))
|
||||
{
|
||||
return Result.Failure("A file already exists at the new path");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(newPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// Read metadata and calculate hash before moving
|
||||
var metaResult = await ReadMetaFileAsync(oldFullPathResult.Value, token);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(metaResult.Message);
|
||||
}
|
||||
|
||||
var fileHash = await CalculateFileHashAsync(oldFullPathResult.Value, token);
|
||||
|
||||
// Move the asset file
|
||||
File.Move(oldFullPathResult.Value, newPath);
|
||||
|
||||
// Move the .gmeta file
|
||||
var oldMetaPath = oldFullPathResult.Value + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
if (File.Exists(oldMetaPath))
|
||||
{
|
||||
File.Move(oldMetaPath, newMetaPath);
|
||||
}
|
||||
|
||||
// Update database directly (bypassing file watcher)
|
||||
await UpsertAssetAsync(newPath, metaResult.Value, fileHash, null, token);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to move asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move an asset to a new location by path.
|
||||
/// </summary>
|
||||
/// <param name="oldPath">Current path of the asset.</param>
|
||||
/// <param name="newPath">New path for the asset (relative or absolute).</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static ValueTask<Result> MoveAssetAsync(string oldPath, string newPath, CancellationToken token = default)
|
||||
{
|
||||
var guidResult = PathToGuid(oldPath);
|
||||
if (guidResult.IsFailure)
|
||||
{
|
||||
return ValueTask.FromResult(Result.Failure(guidResult.Message));
|
||||
}
|
||||
|
||||
return MoveAssetAsync(guidResult.Value, newPath, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy an asset to a new location with a new GUID.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset to copy.</param>
|
||||
/// <param name="newPath">New path for the copied asset (relative or absolute).</param>
|
||||
/// <returns>Result containing the new asset's GUID.</returns>
|
||||
public static async ValueTask<Result<Guid>> CopyAssetAsync(Guid guid, string newPath, CancellationToken token = default)
|
||||
{
|
||||
var oldPathResult = GuidToPath(guid);
|
||||
if (oldPathResult.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure(oldPathResult.Message);
|
||||
}
|
||||
|
||||
var oldFullPathResult = GetFullPath(oldPathResult.Value);
|
||||
if (oldFullPathResult.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure(oldFullPathResult.Message);
|
||||
}
|
||||
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
return Result<Guid>.Failure("AssetsDirectory not initialized");
|
||||
}
|
||||
|
||||
// Ensure new path is absolute and within assets directory
|
||||
if (!Path.IsPathRooted(newPath))
|
||||
{
|
||||
newPath = Path.Combine(AssetsDirectory.FullName, newPath);
|
||||
}
|
||||
|
||||
if (!newPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Result<Guid>.Failure("New path must be within the Assets directory");
|
||||
}
|
||||
|
||||
if (File.Exists(newPath))
|
||||
{
|
||||
return Result<Guid>.Failure("A file already exists at the new path");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(newPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await using var oldFs = File.OpenRead(oldFullPathResult.Value);
|
||||
await using var newFs = File.Create(newPath);
|
||||
await oldFs.CopyToAsync(newFs, token);
|
||||
|
||||
// Generate new metadata with new GUID
|
||||
await GenerateMetaFileAsync(newPath, token);
|
||||
|
||||
// Get the new GUID
|
||||
var newGuidResult = PathToGuid(newPath);
|
||||
if (newGuidResult.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure(newGuidResult.Message);
|
||||
}
|
||||
|
||||
return newGuidResult.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<Guid>.Failure($"Failed to copy asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy an asset to a new location by path.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">Path of the asset to copy.</param>
|
||||
/// <param name="destPath">New path for the copied asset (relative or absolute).</param>
|
||||
/// <returns>Result containing the new asset's GUID.</returns>
|
||||
public static ValueTask<Result<Guid>> CopyAssetAsync(string sourcePath, string destPath, CancellationToken token = default)
|
||||
{
|
||||
var guidResult = PathToGuid(sourcePath);
|
||||
if (guidResult.IsFailure)
|
||||
{
|
||||
return new ValueTask<Result<Guid>>(Task.FromResult(Result<Guid>.Failure(guidResult.Message)));
|
||||
}
|
||||
|
||||
return CopyAssetAsync(guidResult.Value, destPath, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark an asset as dirty for re-importing (in-memory only).
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset to mark dirty.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static Result MarkDirtyAsync(Guid guid, CancellationToken token = default)
|
||||
{
|
||||
MarkDirty(guid);
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import all dirty assets.
|
||||
/// </summary>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static async Task<Result> ImportDirtyAssetsAsync(CancellationToken token = default)
|
||||
{
|
||||
var dirtyGuids = GetDirtyAssets();
|
||||
|
||||
foreach (var guid in dirtyGuids)
|
||||
{
|
||||
var pathResult = GuidToPath(guid);
|
||||
if (pathResult.IsFailure)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullPathResult = GetFullPath(pathResult.Value);
|
||||
if (fullPathResult.IsFailure)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = await ImportAssetAsync(fullPathResult.Value, token);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ClearDirty(guid);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
private static readonly Dictionary<Type, AssetImporter> s_importerInstances = new();
|
||||
|
||||
/// <summary>
|
||||
/// Import an asset at the specified path.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Full path to the asset file.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
private static async ValueTask<Result> ImportAssetAsync(string assetPath, CancellationToken token = default)
|
||||
{
|
||||
var extension = Path.GetExtension(assetPath);
|
||||
|
||||
if (!s_importerTypeLookup.TryGetValue(extension, out var importerType))
|
||||
{
|
||||
// No importer registered for this file type
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
// Get or create importer instance
|
||||
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
|
||||
{
|
||||
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
|
||||
if (importerInstance is null)
|
||||
{
|
||||
return Result.Failure($"Failed to create importer instance for type {importerType.Name}");
|
||||
}
|
||||
|
||||
s_importerInstances[importerType] = importerInstance;
|
||||
}
|
||||
|
||||
// Read metadata
|
||||
var metaResult = await ReadMetaFileAsync(assetPath, token);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
return Result.Failure($"Failed to read asset metadata: {metaResult.Message}");
|
||||
}
|
||||
|
||||
return await importerInstance.ImportAsync(assetPath, metaResult.Value, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the importer type for a specific file extension.
|
||||
/// </summary>
|
||||
/// <param name="extension">File extension (e.g., ".png").</param>
|
||||
/// <returns>The importer type if found, otherwise null.</returns>
|
||||
public static Type? GetImporterType(string extension)
|
||||
{
|
||||
s_importerTypeLookup.TryGetValue(extension, out var importerType);
|
||||
return importerType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered importer types and their supported extensions.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary mapping extensions to importer types.</returns>
|
||||
public static Dictionary<string, Type> GetAllImporters()
|
||||
{
|
||||
return new Dictionary<string, Type>(s_importerTypeLookup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export in-memory asset data to disk.
|
||||
/// The importer will serialize the data into a format it can later import.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of asset data to export.</typeparam>
|
||||
/// <param name="assetPath">Full path where the asset should be saved.</param>
|
||||
/// <param name="assetData">In-memory asset data to export.</param>
|
||||
/// <returns>Result with the GUID of the exported asset.</returns>
|
||||
public static async ValueTask<Result<Guid>> ExportAssetAsync<T>(string assetPath, T assetData, CancellationToken token = default) where T : class
|
||||
{
|
||||
var extension = Path.GetExtension(assetPath);
|
||||
|
||||
if (!s_importerTypeLookup.TryGetValue(extension, out var importerType))
|
||||
{
|
||||
return Result<Guid>.Failure($"No importer registered for extension {extension}");
|
||||
}
|
||||
|
||||
// Get or create importer instance
|
||||
if (!s_importerInstances.TryGetValue(importerType, out var importerInstance))
|
||||
{
|
||||
importerInstance = Activator.CreateInstance(importerType) as AssetImporter;
|
||||
if (importerInstance is null)
|
||||
{
|
||||
return Result<Guid>.Failure($"Failed to create importer instance for type {importerType.Name}");
|
||||
}
|
||||
|
||||
s_importerInstances[importerType] = importerInstance;
|
||||
}
|
||||
|
||||
// Find and invoke the ExportAsync method
|
||||
var exportMethod = importerType.GetMethod("ExportAsync", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (exportMethod == null)
|
||||
{
|
||||
return Result<Guid>.Failure($"ExportAsync method not found on importer {importerType.Name}. This importer does not support exporting.");
|
||||
}
|
||||
|
||||
// Generate metadata for the new asset
|
||||
var result = await GenerateMetaFileAsync(assetPath, token);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure($"Failed to generate metadata: {result.Message}");
|
||||
}
|
||||
|
||||
var metaResult = await ReadMetaFileAsync(assetPath, token);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure($"Failed to read metadata: {metaResult.Message}");
|
||||
}
|
||||
|
||||
result = await importerInstance.ExportAsync(assetPath, assetData, metaResult.Value, token);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure(result.Message);
|
||||
}
|
||||
|
||||
// Calculate file hash and update database
|
||||
var fileHash = await CalculateFileHashAsync(assetPath, token);
|
||||
await UpsertAssetAsync(assetPath, metaResult.Value, fileHash, null, token);
|
||||
|
||||
return metaResult.Value.Guid;
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Data.Services;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
// Asset cache - stores loaded assets by GUID
|
||||
private static readonly ConcurrentDictionary<Guid, Asset> s_assetCache = new();
|
||||
|
||||
// LRU tracking - stores access time for each cached asset
|
||||
private static readonly ConcurrentDictionary<Guid, DateTime> s_assetAccessTime = new();
|
||||
|
||||
// Maximum number of cached assets before eviction starts
|
||||
private const int MAX_CACHED_ASSETS = 1000;
|
||||
|
||||
// Percentage of cache to evict when limit is reached (evict oldest 20%)
|
||||
private const float _CACHE_EVICTION_PERCENTAGE = 0.2f;
|
||||
|
||||
private static Result<string> GetImportedAssetsDirectory()
|
||||
{
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
return Result<string>.Failure("AssetsDirectory not initialized");
|
||||
}
|
||||
|
||||
var cacheDir = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "ImportedAssets");
|
||||
if (!Directory.Exists(cacheDir))
|
||||
{
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
}
|
||||
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
private static Result<string> GetImportedAssetPath(Guid guid)
|
||||
{
|
||||
var importedDirResult = GetImportedAssetsDirectory();
|
||||
if (importedDirResult.IsFailure)
|
||||
{
|
||||
return Result<string>.Failure(importedDirResult.Message);
|
||||
}
|
||||
|
||||
// Store imported assets as {GUID}.asset
|
||||
var assetDataPath = Path.Combine(importedDirResult.Value, $"{guid}.asset");
|
||||
return assetDataPath;
|
||||
}
|
||||
|
||||
private static Result<T> LoadAssetInternal<T>(Guid guid) where T : Asset
|
||||
{
|
||||
// Check cache first
|
||||
if (s_assetCache.TryGetValue(guid, out var cachedAsset))
|
||||
{
|
||||
// Update access time for LRU
|
||||
s_assetAccessTime[guid] = DateTime.UtcNow;
|
||||
|
||||
if (cachedAsset is T typedAsset)
|
||||
{
|
||||
return typedAsset;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Result<T>.Failure($"Cached asset is of type {cachedAsset.GetType().Name}, expected {typeof(T).Name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Asset not in cache, load from disk
|
||||
var assetPathResult = GetImportedAssetPath(guid);
|
||||
if (assetPathResult.IsFailure)
|
||||
{
|
||||
return Result<T>.Failure(assetPathResult.Message);
|
||||
}
|
||||
|
||||
var assetDataPath = assetPathResult.Value;
|
||||
if (!File.Exists(assetDataPath))
|
||||
{
|
||||
return Result<T>.Failure($"Imported asset data not found at {assetDataPath}. Asset may not have been imported yet.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read and deserialize asset data
|
||||
var json = File.ReadAllText(assetDataPath);
|
||||
var asset = JsonSerializer.Deserialize<T>(json);
|
||||
if (asset == null)
|
||||
{
|
||||
return Result<T>.Failure("Failed to deserialize asset data");
|
||||
}
|
||||
|
||||
// Add to cache
|
||||
CacheAsset(guid, asset);
|
||||
return asset;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<T>.Failure($"Failed to load asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static Result<T> LoadAssetAtPath<T>(string assetPath) where T : Asset
|
||||
{
|
||||
var guidResult = PathToGuid(assetPath);
|
||||
if (guidResult.IsFailure)
|
||||
{
|
||||
return Result<T>.Failure(guidResult.Message);
|
||||
}
|
||||
|
||||
return LoadAsset<T>(guidResult.Value);
|
||||
}
|
||||
|
||||
private static void CacheAsset(Guid guid, Asset asset)
|
||||
{
|
||||
// Check if we need to evict old assets
|
||||
if (s_assetCache.Count >= MAX_CACHED_ASSETS)
|
||||
{
|
||||
EvictOldestAssets();
|
||||
}
|
||||
|
||||
s_assetCache[guid] = asset;
|
||||
s_assetAccessTime[guid] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private static void EvictOldestAssets()
|
||||
{
|
||||
var evictionCount = (int)(MAX_CACHED_ASSETS * _CACHE_EVICTION_PERCENTAGE);
|
||||
|
||||
// Sort by access time and remove oldest entries
|
||||
var oldestAssets = s_assetAccessTime
|
||||
.OrderBy(kvp => kvp.Value)
|
||||
.Take(evictionCount)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var guid in oldestAssets)
|
||||
{
|
||||
s_assetCache.TryRemove(guid, out _);
|
||||
s_assetAccessTime.TryRemove(guid, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unload a specific asset from cache.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset to unload.</param>
|
||||
public static void UnloadAsset(Guid guid)
|
||||
{
|
||||
s_assetCache.TryRemove(guid, out _);
|
||||
s_assetAccessTime.TryRemove(guid, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unload all assets from cache.
|
||||
/// </summary>
|
||||
public static void UnloadAllAssets()
|
||||
{
|
||||
s_assetCache.Clear();
|
||||
s_assetAccessTime.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if an asset is currently loaded in cache.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset.</param>
|
||||
/// <returns>True if the asset is in cache.</returns>
|
||||
public static bool IsAssetLoaded(Guid guid)
|
||||
{
|
||||
return s_assetCache.ContainsKey(guid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get cache statistics.
|
||||
/// </summary>
|
||||
/// <returns>Tuple of (current cache size, max cache size).</returns>
|
||||
public static (int currentSize, int maxSize) GetCacheStats()
|
||||
{
|
||||
return (s_assetCache.Count, MAX_CACHED_ASSETS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save an imported asset to disk for later loading.
|
||||
/// This should be called by importers after processing the source file.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of asset data.</typeparam>
|
||||
/// <param name="guid">GUID of the asset.</param>
|
||||
/// <param name="assetData">Processed asset data to save.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static Result SaveImportedAsset<T>(Guid guid, T assetData)
|
||||
where T : Asset
|
||||
{
|
||||
var assetPathResult = GetImportedAssetPath(guid);
|
||||
if (assetPathResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(assetPathResult.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(assetData, s_defaultJsonOptions);
|
||||
File.WriteAllText(assetPathResult.Value, json);
|
||||
|
||||
// Invalidate cache for this asset so it gets reloaded next time
|
||||
UnloadAsset(guid);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to save imported asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the relative path from the assets directory.
|
||||
/// </summary>
|
||||
private static Result<string> GetRelativePath(string fullPath)
|
||||
{
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
return Result<string>.Failure("AssetsDirectory not initialized");
|
||||
}
|
||||
|
||||
if (!fullPath.StartsWith(AssetsDirectory.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Result<string>.Failure("Path is not within assets directory");
|
||||
}
|
||||
|
||||
return Path.GetRelativePath(AssetsDirectory.FullName, fullPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the full path from a relative path.
|
||||
/// </summary>
|
||||
private static Result<string> GetFullPath(string relativePath)
|
||||
{
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
return Result<string>.Failure("AssetsDirectory not initialized");
|
||||
}
|
||||
|
||||
return Path.Combine(AssetsDirectory.FullName, relativePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find GUID by asset path.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Full or relative path to the asset.</param>
|
||||
/// <returns>The GUID of the asset if found.</returns>
|
||||
public static Result<Guid> PathToGuid(string assetPath)
|
||||
{
|
||||
var relativePath = assetPath;
|
||||
|
||||
// Convert to relative path if it's a full path
|
||||
if (Path.IsPathRooted(assetPath))
|
||||
{
|
||||
var relResult = GetRelativePath(assetPath);
|
||||
if (relResult.IsFailure)
|
||||
{
|
||||
return Result<Guid>.Failure(relResult.Message);
|
||||
}
|
||||
relativePath = relResult.Value;
|
||||
}
|
||||
|
||||
// Normalize path separators
|
||||
relativePath = relativePath.Replace('\\', '/');
|
||||
|
||||
lock (s_dbLock)
|
||||
{
|
||||
if (s_pathAssetLookup.TryGetValue(relativePath, out var guid))
|
||||
{
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
|
||||
return Result<Guid>.Failure("Asset not found in database");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find path by GUID.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset.</param>
|
||||
/// <returns>The relative path to the asset if found.</returns>
|
||||
public static Result<string> GuidToPath(Guid guid)
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
if (s_assetPathLookup.TryGetValue(guid, out var path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return Result<string>.Failure("Asset GUID not found in database");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load asset by GUID with caching.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of asset to load.</typeparam>
|
||||
/// <param name="guid">GUID of the asset.</param>
|
||||
/// <returns>The loaded asset.</returns>
|
||||
public static Result<T> LoadAsset<T>(Guid guid) where T : Asset
|
||||
{
|
||||
// Implemented in AssetDatabase.Loader.cs
|
||||
return LoadAssetInternal<T>(guid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get asset tags by GUID.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset.</param>
|
||||
/// <returns>List of tags associated with the asset.</returns>
|
||||
public static async ValueTask<Result<List<string>>> GetAssetTagsAsync(Guid guid, CancellationToken token = default)
|
||||
{
|
||||
var pathResult = GuidToPath(guid);
|
||||
if (pathResult.IsFailure)
|
||||
{
|
||||
return Result<List<string>>.Failure(pathResult.Message);
|
||||
}
|
||||
|
||||
var fullPathResult = GetFullPath(pathResult.Value);
|
||||
if (fullPathResult.IsFailure)
|
||||
{
|
||||
return Result<List<string>>.Failure(fullPathResult.Message);
|
||||
}
|
||||
|
||||
var metaResult = await ReadMetaFileAsync(fullPathResult.Value, token);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
return Result<List<string>>.Failure(metaResult.Message);
|
||||
}
|
||||
|
||||
return metaResult.Value.Tags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set asset tags by GUID.
|
||||
/// </summary>
|
||||
/// <param name="guid">GUID of the asset.</param>
|
||||
/// <param name="tags">New tags for the asset.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public static async ValueTask<Result> SetAssetTagsAsync(Guid guid, List<string> tags, CancellationToken token = default)
|
||||
{
|
||||
var pathResult = GuidToPath(guid);
|
||||
if (pathResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(pathResult.Message);
|
||||
}
|
||||
|
||||
var fullPathResult = GetFullPath(pathResult.Value);
|
||||
if (fullPathResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(fullPathResult.Message);
|
||||
}
|
||||
|
||||
var metaResult = await ReadMetaFileAsync(fullPathResult.Value, token);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(metaResult.Message);
|
||||
}
|
||||
|
||||
metaResult.Value.Tags = tags;
|
||||
|
||||
// Write updated metadata to .gmeta file
|
||||
var writeResult = await WriteMetaFileAsync(fullPathResult.Value + Utilities.FileExtensions.META_FILE_EXTENSION, metaResult.Value, token);
|
||||
if (writeResult.IsFailure)
|
||||
{
|
||||
return writeResult;
|
||||
}
|
||||
|
||||
// Update database with new tags
|
||||
var fileHash = await CalculateFileHashAsync(fullPathResult.Value, token);
|
||||
return await UpsertAssetAsync(fullPathResult.Value, metaResult.Value, fileHash, null, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search assets by name pattern.
|
||||
/// Supports SQL LIKE wildcards: * (any characters) and ? (single character).
|
||||
/// </summary>
|
||||
/// <param name="namePattern">Search pattern (e.g., "*.txt", "player?", "test*").</param>
|
||||
/// <returns>List of matching asset GUIDs.</returns>
|
||||
public static async Task<List<Guid>> FindAssetsByNameAsync(string namePattern, CancellationToken token = default)
|
||||
{
|
||||
return await GetAssetsByNameAsync(namePattern, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find assets by tag.
|
||||
/// </summary>
|
||||
/// <param name="tag">Tag to search for.</param>
|
||||
/// <returns>List of asset GUIDs with the specified tag.</returns>
|
||||
public static async Task<List<Guid>> FindAssetsByTagAsync(string tag, CancellationToken token = default)
|
||||
{
|
||||
return await GetAssetsByTagAsync(tag, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all assets in the database.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary mapping GUIDs to relative paths.</returns>
|
||||
public static IReadOnlyDictionary<Guid, string> GetAllAssets()
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
return s_assetPathLookup.AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core.Utilities;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
private static readonly Dictionary<string, Type> s_importerTypeLookup = new();
|
||||
|
||||
private static void InitializeMetaData()
|
||||
{
|
||||
if (s_watcher == null)
|
||||
{
|
||||
throw new InvalidOperationException("AssetDatabase is not initialized. Ensure that Initialize() is called before registering asset importers.");
|
||||
}
|
||||
|
||||
var importerTypes = TypeCache.GetTypes().Where(t => t.GetCustomAttribute<AssetImporterAttribute>() != null);
|
||||
foreach (var type in importerTypes)
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<AssetImporterAttribute>()!;
|
||||
foreach (var extension in attribute.SupportedExtensions)
|
||||
{
|
||||
s_importerTypeLookup[extension] = type;
|
||||
}
|
||||
}
|
||||
|
||||
s_watcher.Created += OnFSEvent;
|
||||
s_watcher.Deleted += OnFSEvent;
|
||||
s_watcher.Changed += OnFSEvent;
|
||||
s_watcher.Renamed += OnAssetRenamed;
|
||||
}
|
||||
|
||||
private static Result<string> GetMetaFilePath(string assetPath)
|
||||
{
|
||||
if (Directory.Exists(assetPath))
|
||||
{
|
||||
return Result<string>.Failure("Cannot create metadata for directories");
|
||||
}
|
||||
|
||||
if (Path.GetExtension(assetPath).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Result<string>.Failure("Cannot create metadata for metadata files");
|
||||
}
|
||||
|
||||
return assetPath + FileExtensions.META_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
private static ImporterSettings? GetDefaultSettingsForAsset(string assetPath)
|
||||
{
|
||||
var extension = Path.GetExtension(assetPath);
|
||||
|
||||
if (s_importerTypeLookup.TryGetValue(extension, out var importerType))
|
||||
{
|
||||
var settingsType = importerType.BaseType?.GetGenericArguments()[0];
|
||||
if (settingsType == null || !typeof(ImporterSettings).IsAssignableFrom(settingsType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (ImporterSettings?)Activator.CreateInstance(settingsType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate SHA256 hash of a file for change detection.
|
||||
/// </summary>
|
||||
private static async Task<string> CalculateFileHashAsync(string filePath, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
var hash = await SHA256.HashDataAsync(stream, token);
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Result> WriteMetaFileAsync(string metaFilePath, AssetMeta metaData, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var fileStream = File.Create(metaFilePath);
|
||||
await JsonSerializer.SerializeAsync(fileStream, metaData, s_defaultJsonOptions, token);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read metadata from a .gmeta file.
|
||||
/// </summary>
|
||||
private static async ValueTask<Result<AssetMeta>> ReadMetaFileAsync(string assetPath, CancellationToken token = default)
|
||||
{
|
||||
var metaFileResult = GetMetaFilePath(assetPath);
|
||||
if (metaFileResult.IsFailure)
|
||||
{
|
||||
return Result<AssetMeta>.Failure(metaFileResult.Message);
|
||||
}
|
||||
|
||||
if (!File.Exists(metaFileResult.Value))
|
||||
{
|
||||
return Result<AssetMeta>.Failure("Metadata file does not exist");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var fileStream = File.OpenRead(metaFileResult.Value);
|
||||
var meta = await JsonSerializer.DeserializeAsync<AssetMeta>(fileStream, s_defaultJsonOptions, token);
|
||||
if (meta == null)
|
||||
{
|
||||
return Result<AssetMeta>.Failure("Failed to deserialize metadata");
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<AssetMeta>.Failure($"Failed to read metadata: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static async ValueTask<Result> GenerateMetaFileAsync(string assetPath, CancellationToken token = default)
|
||||
{
|
||||
Result r;
|
||||
|
||||
var metaFileResult = GetMetaFilePath(assetPath);
|
||||
if (metaFileResult.IsFailure)
|
||||
{
|
||||
return Result.Failure(metaFileResult.Message);
|
||||
}
|
||||
|
||||
if (File.Exists(metaFileResult.Value))
|
||||
{
|
||||
var existingMetaResult = await ReadMetaFileAsync(assetPath, token);
|
||||
if (existingMetaResult.IsSuccess)
|
||||
{
|
||||
var existingMeta = existingMetaResult.Value;
|
||||
if (s_assetPathLookup.TryGetValue(existingMeta.Guid, out var path))
|
||||
{
|
||||
var relResult = GetRelativePath(assetPath);
|
||||
if (relResult.IsSuccess && assetPath != path)
|
||||
{
|
||||
// GUID conflict - regenerate
|
||||
existingMeta.Guid = Guid.NewGuid();
|
||||
r = await WriteMetaFileAsync(metaFileResult.Value, existingMeta, token);
|
||||
if (r.IsFailure)
|
||||
{
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate file hash and update database
|
||||
var fileHash = await CalculateFileHashAsync(assetPath, token);
|
||||
await UpsertAssetAsync(assetPath, existingMeta, fileHash, null, token);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate initial file hash
|
||||
var fileHash2 = await CalculateFileHashAsync(assetPath, token);
|
||||
|
||||
var defaultSettings = GetDefaultSettingsForAsset(assetPath);
|
||||
var metaData = new AssetMeta
|
||||
{
|
||||
Guid = Guid.NewGuid()
|
||||
};
|
||||
|
||||
if (defaultSettings != null)
|
||||
{
|
||||
metaData.SetImporterSettings(defaultSettings.GetType().Name, defaultSettings);
|
||||
}
|
||||
|
||||
r = await WriteMetaFileAsync(metaFileResult.Value, metaData, token);
|
||||
if (r.IsFailure)
|
||||
{
|
||||
return r;
|
||||
}
|
||||
|
||||
// Add to database
|
||||
await UpsertAssetAsync(assetPath, metaData, fileHash2, null, token);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsMetaFile(string path)
|
||||
{
|
||||
return Path.GetExtension(path).Equals(FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static async void OnFSEvent(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (IsMetaFile(e.FullPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var type = e.ChangeType switch
|
||||
{
|
||||
WatcherChangeTypes.Created => AssetCommandType.FileCreated,
|
||||
WatcherChangeTypes.Deleted => AssetCommandType.FileDeleted,
|
||||
WatcherChangeTypes.Changed => AssetCommandType.FileModified,
|
||||
_ => throw new InvalidOperationException("Unsupported file system event type")
|
||||
};
|
||||
|
||||
await PostCommandAsync(new AssetCommand(type, e.FullPath, Timestamp: DateTime.UtcNow));
|
||||
}
|
||||
|
||||
private static async void OnAssetRenamed(object sender, RenamedEventArgs e)
|
||||
{
|
||||
if (IsMetaFile(e.FullPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await PostCommandAsync(new AssetCommand(AssetCommandType.FileRenamed, e.FullPath, e.OldFullPath, DateTime.UtcNow));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark all assets that depend on the specified asset as dirty.
|
||||
/// </summary>
|
||||
private static async Task MarkDependentAssetsDirtyAsync(Guid assetGuid)
|
||||
{
|
||||
// TODO: We should have a reverse dependency lookup in the database to avoid scanning all assets.
|
||||
|
||||
// Query database for all assets and check their dependencies
|
||||
var allAssets = GetAllAssets();
|
||||
|
||||
foreach (var kvp in allAssets)
|
||||
{
|
||||
var dependencies = await GetDependenciesAsync(kvp.Key, CancellationToken.None);
|
||||
if (dependencies.Contains(assetGuid))
|
||||
{
|
||||
MarkDirty(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core.Utilities;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
private static readonly Dictionary<string, Action<string>> s_assetOpenHandlers = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void InitializeAssetHandle()
|
||||
{
|
||||
var methods = TypeCache.GetTypes()
|
||||
.SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
.Where(m => m.GetCustomAttribute<AssetOpenHandlerAttribute>() != null &&
|
||||
m.GetParameters().Length == 1 &&
|
||||
m.GetParameters()[0].ParameterType == typeof(string));
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var attr = method.GetCustomAttribute<AssetOpenHandlerAttribute>()!;
|
||||
var del = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), method);
|
||||
foreach (var ext in attr.Extensions)
|
||||
{
|
||||
if (s_assetOpenHandlers.ContainsKey(ext))
|
||||
{
|
||||
Logger.LogError($"Duplicate asset open handler for extension '{ext}' found in method '{method.Name}'. Existing handler will be overwritten.");
|
||||
}
|
||||
|
||||
s_assetOpenHandlers[ext] = del;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenAsset(string path)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
if (s_assetOpenHandlers.TryGetValue(extension, out var handler))
|
||||
{
|
||||
handler(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Data.Services;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
private static SqliteConnection? s_dbConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the SQLite database for asset caching.
|
||||
/// </summary>
|
||||
private static async Task InitializeDatabaseAsync(CancellationToken token = default)
|
||||
{
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
throw new InvalidOperationException("AssetsDirectory is not set. Initialize() must be called first.");
|
||||
}
|
||||
|
||||
var dbPath = Path.Combine(AssetsDirectory.Parent!.FullName, ProjectService.CACHE_FOLDER, "AssetDatabase.db");
|
||||
var cacheDir = Path.GetDirectoryName(dbPath);
|
||||
if (!Directory.Exists(cacheDir))
|
||||
{
|
||||
Directory.CreateDirectory(cacheDir!);
|
||||
}
|
||||
|
||||
var connectionString = new SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = dbPath,
|
||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||
Cache = SqliteCacheMode.Shared
|
||||
}.ToString();
|
||||
|
||||
s_dbConnection = new SqliteConnection(connectionString);
|
||||
await s_dbConnection.OpenAsync(token);
|
||||
|
||||
// Create tables
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS Assets (
|
||||
Guid TEXT PRIMARY KEY,
|
||||
Path TEXT NOT NULL,
|
||||
Version INTEGER NOT NULL,
|
||||
Tags TEXT,
|
||||
FileHash TEXT,
|
||||
DependencyGuids TEXT,
|
||||
LastModified INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_path ON Assets(Path);
|
||||
";
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add or update an asset in the database.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Full path to the asset file.</param>
|
||||
/// <param name="meta">Asset metadata from .gmeta file.</param>
|
||||
/// <param name="fileHash">SHA256 hash of the asset file content.</param>
|
||||
/// <param name="dependencies">List of GUIDs this asset depends on (extracted during import).</param>
|
||||
private static async ValueTask<Result> UpsertAssetAsync(string assetPath, AssetMeta meta, string fileHash, List<Guid>? dependencies = null, CancellationToken token = default)
|
||||
{
|
||||
if (s_dbConnection == null)
|
||||
{
|
||||
return Result.Failure("Database not initialized");
|
||||
}
|
||||
|
||||
var relativePath = GetRelativePath(assetPath);
|
||||
if (relativePath.IsFailure)
|
||||
{
|
||||
return Result.Failure(relativePath.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
// If this GUID already exists with a different path, remove the old path mapping
|
||||
if (s_assetPathLookup.TryGetValue(meta.Guid, out var oldPath) && oldPath != relativePath.Value)
|
||||
{
|
||||
s_pathAssetLookup.Remove(oldPath);
|
||||
}
|
||||
|
||||
// Update lookups with new path (normalize path separators for consistency)
|
||||
var normalizedPath = relativePath.Value.Replace('\\', '/');
|
||||
s_assetPathLookup[meta.Guid] = normalizedPath;
|
||||
s_pathAssetLookup[normalizedPath] = meta.Guid;
|
||||
}
|
||||
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
INSERT OR REPLACE INTO Assets (Guid, Path, Version, Tags, FileHash, DependencyGuids, LastModified)
|
||||
VALUES (@guid, @path, @version, @tags, @fileHash, @deps, @modified)
|
||||
";
|
||||
cmd.Parameters.AddWithValue("@guid", meta.Guid.ToString());
|
||||
cmd.Parameters.AddWithValue("@path", relativePath.Value);
|
||||
cmd.Parameters.AddWithValue("@version", meta.Version);
|
||||
cmd.Parameters.AddWithValue("@tags", JsonSerializer.Serialize(meta.Tags));
|
||||
cmd.Parameters.AddWithValue("@fileHash", fileHash);
|
||||
cmd.Parameters.AddWithValue("@deps", JsonSerializer.Serialize(dependencies ?? new List<Guid>()));
|
||||
cmd.Parameters.AddWithValue("@modified", DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(token);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to upsert asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an asset from the database.
|
||||
/// </summary>
|
||||
private static async Task<Result> RemoveAssetFromDatabaseAsync(Guid guid, CancellationToken token = default)
|
||||
{
|
||||
if (s_dbConnection == null)
|
||||
{
|
||||
return Result.Failure("Database not initialized");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
if (s_assetPathLookup.TryGetValue(guid, out var path))
|
||||
{
|
||||
s_assetPathLookup.Remove(guid);
|
||||
s_pathAssetLookup.Remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM Assets WHERE Guid = @guid";
|
||||
cmd.Parameters.AddWithValue("@guid", guid.ToString());
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(token);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to remove asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Load all assets from the database into memory cache.
|
||||
/// </summary>
|
||||
private static async Task LoadAssetCacheFromDatabaseAsync(CancellationToken token = default)
|
||||
{
|
||||
if (s_dbConnection == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Guid, Path FROM Assets";
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(token);
|
||||
while (await reader.ReadAsync(token))
|
||||
{
|
||||
var guidStr = reader.GetString(0);
|
||||
var path = reader.GetString(1);
|
||||
|
||||
if (Guid.TryParse(guidStr, out var guid))
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
s_assetPathLookup[guid] = path;
|
||||
s_pathAssetLookup[path] = guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load asset cache: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get assets by tag.
|
||||
/// </summary>
|
||||
private static async Task<List<Guid>> GetAssetsByTagAsync(string tag, CancellationToken token = default)
|
||||
{
|
||||
var result = new List<Guid>();
|
||||
|
||||
if (s_dbConnection == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Guid, Tags FROM Assets";
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(token);
|
||||
while (await reader.ReadAsync(token))
|
||||
{
|
||||
var guidStr = reader.GetString(0);
|
||||
var tagsJson = reader.GetString(1);
|
||||
|
||||
if (Guid.TryParse(guidStr, out var guid))
|
||||
{
|
||||
var tags = JsonSerializer.Deserialize<List<string>>(tagsJson);
|
||||
if (tags != null && tags.Contains(tag, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Add(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the file hash for an asset from the database.
|
||||
/// </summary>
|
||||
private static async Task<string?> GetFileHashAsync(Guid guid, CancellationToken token = default)
|
||||
{
|
||||
if (s_dbConnection == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = "SELECT FileHash FROM Assets WHERE Guid = @guid";
|
||||
cmd.Parameters.AddWithValue("@guid", guid.ToString());
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync(token);
|
||||
return result?.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the dependencies for an asset from the database.
|
||||
/// </summary>
|
||||
private static async Task<List<Guid>> GetDependenciesAsync(Guid guid, CancellationToken token = default)
|
||||
{
|
||||
if (s_dbConnection == null)
|
||||
{
|
||||
return new List<Guid>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = "SELECT DependencyGuids FROM Assets WHERE Guid = @guid";
|
||||
cmd.Parameters.AddWithValue("@guid", guid.ToString());
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync(token);
|
||||
if (result != null)
|
||||
{
|
||||
var json = result.ToString();
|
||||
return JsonSerializer.Deserialize<List<Guid>>(json ?? "[]") ?? new List<Guid>();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
return new List<Guid>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find assets by name pattern using database query with wildcards.
|
||||
/// </summary>
|
||||
/// <param name="namePattern">Pattern supporting * (any chars) and ? (single char).</param>
|
||||
private static async Task<List<Guid>> GetAssetsByNameAsync(string namePattern, CancellationToken token = default)
|
||||
{
|
||||
var results = new List<Guid>();
|
||||
|
||||
if (s_dbConnection == null)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Convert wildcard pattern to SQL LIKE pattern
|
||||
var sqlPattern = namePattern.Replace('*', '%').Replace('?', '_');
|
||||
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
|
||||
// Extract just the filename from the path for matching
|
||||
// SQLite doesn't have a built-in path manipulation, so we search in the full path
|
||||
// and filter by checking if the pattern matches the filename part
|
||||
cmd.CommandText = @"
|
||||
SELECT Guid, Path FROM Assets
|
||||
WHERE Path LIKE '%' || @pattern || '%'
|
||||
";
|
||||
cmd.Parameters.AddWithValue("@pattern", sqlPattern);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(token);
|
||||
while (await reader.ReadAsync(token))
|
||||
{
|
||||
var guidStr = reader.GetString(0);
|
||||
var path = reader.GetString(1);
|
||||
|
||||
// Extract filename and check if it matches the pattern
|
||||
var fileName = Path.GetFileName(path);
|
||||
|
||||
// Convert pattern to regex for proper matching
|
||||
var regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(namePattern)
|
||||
.Replace("\\*", ".*")
|
||||
.Replace("\\?", ".") + "$";
|
||||
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(fileName, regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
||||
{
|
||||
if (Guid.TryParse(guidStr, out var guid))
|
||||
{
|
||||
results.Add(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove orphaned entries from database (assets that no longer exist on disk).
|
||||
/// </summary>
|
||||
private static async Task RemoveOrphanedEntriesAsync(CancellationToken token = default)
|
||||
{
|
||||
if (s_dbConnection == null || AssetsDirectory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var orphanedGuids = new List<Guid>();
|
||||
|
||||
await using var cmd = s_dbConnection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Guid, Path FROM Assets";
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(token);
|
||||
while (await reader.ReadAsync(token))
|
||||
{
|
||||
var guidStr = reader.GetString(0);
|
||||
var path = reader.GetString(1);
|
||||
|
||||
if (Guid.TryParse(guidStr, out var guid))
|
||||
{
|
||||
// Check if file exists
|
||||
var fullPath = Path.Combine(AssetsDirectory.FullName, path);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
orphanedGuids.Add(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove orphaned entries
|
||||
foreach (var guid in orphanedGuids)
|
||||
{
|
||||
await RemoveAssetFromDatabaseAsync(guid, token);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail - cleanup is best effort
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,531 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Data.Services;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Command types for asset database operations.
|
||||
/// </summary>
|
||||
internal enum AssetCommandType
|
||||
{
|
||||
FileCreated,
|
||||
FileModified,
|
||||
FileDeleted,
|
||||
FileRenamed,
|
||||
ManualRefresh
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a command to process an asset operation.
|
||||
/// </summary>
|
||||
internal readonly record struct AssetCommand(
|
||||
AssetCommandType Type,
|
||||
string Path,
|
||||
string? OldPath = null,
|
||||
DateTime Timestamp = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Centralized asset database that manages all assets in the project.
|
||||
/// Handles asset registration, lookup, importing, and dependency management.
|
||||
/// Uses SQLite for persistent storage and efficient querying.
|
||||
/// </summary>
|
||||
public static partial class AssetDatabase
|
||||
{
|
||||
private static FileSystemWatcher? s_watcher;
|
||||
private static readonly Lock s_dbLock = new();
|
||||
private static readonly Dictionary<Guid, string> s_assetPathLookup = new();
|
||||
private static readonly Dictionary<string, Guid> s_pathAssetLookup = new();
|
||||
|
||||
// In-memory dirty asset tracking (for runtime modifications only)
|
||||
// TODO: We do not handle the reimporting of dirty assets yet
|
||||
private static readonly HashSet<Guid> s_dirtyAssets = new();
|
||||
|
||||
// Command buffer pattern - Channel for file system event commands
|
||||
private static Channel<AssetCommand>? s_commandChannel;
|
||||
private static Timer? s_commandProcessorTimer;
|
||||
private static readonly Lock s_commandLock = new();
|
||||
private static readonly ConcurrentQueue<AssetCommand> s_waitingCommands = new(); // Commands waiting for manual refresh
|
||||
private static bool s_autoRefreshEnabled = true;
|
||||
|
||||
// Initialization guard
|
||||
private static readonly Lock s_initializationLock = new();
|
||||
private static bool s_initialized = false;
|
||||
|
||||
private static readonly TimeSpan s_debounceDelay = TimeSpan.FromMilliseconds(100);
|
||||
private static ManualResetEventSlim s_resetEventSlim = new(false);
|
||||
|
||||
private static readonly JsonSerializerOptions s_defaultJsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
|
||||
}
|
||||
};
|
||||
|
||||
public static DirectoryInfo? AssetsDirectory
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the asset database.
|
||||
/// Must be called after project is loaded.
|
||||
/// </summary>
|
||||
|
||||
internal static async Task Initialize(CancellationToken token = default)
|
||||
{
|
||||
lock (s_initializationLock)
|
||||
{
|
||||
if (s_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_initialized = true;
|
||||
}
|
||||
|
||||
if (ProjectService.CurrentProject.Metadata == null)
|
||||
{
|
||||
throw new InvalidOperationException("Project metadata is not initialized. Ensure that the project is loaded before accessing the AssetDatabase.");
|
||||
}
|
||||
|
||||
AssetsDirectory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER));
|
||||
|
||||
s_commandChannel = Channel.CreateUnbounded<AssetCommand>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = false,
|
||||
SingleWriter = false
|
||||
});
|
||||
|
||||
// Initialize command processor timer (starts disabled, triggered by events)
|
||||
s_commandProcessorTimer = new Timer(ProcessPendingCommands, null, Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
await InitializeDatabaseAsync(token);
|
||||
await LoadAssetCacheFromDatabaseAsync(token);
|
||||
|
||||
s_watcher = new FileSystemWatcher
|
||||
{
|
||||
Path = AssetsDirectory.FullName,
|
||||
IncludeSubdirectories = true,
|
||||
EnableRaisingEvents = true,
|
||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
InitializeAssetHandle();
|
||||
InitializeMetaData();
|
||||
|
||||
// TODO: Timestamp fake instead of full scan.
|
||||
await ValidateAndFixDatabaseAsync(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate the asset database and fix any inconsistencies.
|
||||
/// Checks for missing/corrupted assets and regenerates metadata as needed.
|
||||
/// </summary>
|
||||
private static async Task<Result> ValidateAndFixDatabaseAsync(CancellationToken token = default)
|
||||
{
|
||||
if (AssetsDirectory == null)
|
||||
{
|
||||
return Result.Failure("AssetsDirectory not initialized");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Scan all files in assets directory
|
||||
var allFiles = Directory.EnumerateFiles(AssetsDirectory.FullName, "*.*", SearchOption.AllDirectories)
|
||||
.Where(f => !f.EndsWith(Utilities.FileExtensions.META_FILE_EXTENSION, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Ensure all files have metadata
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
var metaPath = file + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
if (!File.Exists(metaPath))
|
||||
{
|
||||
await GenerateMetaFileAsync(file, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Validate and update database
|
||||
var metaResult = await ReadMetaFileAsync(file, token);
|
||||
if (metaResult.IsSuccess)
|
||||
{
|
||||
var fileHash = await CalculateFileHashAsync(file, token);
|
||||
await UpsertAssetAsync(file, metaResult.Value, fileHash, null, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Corrupted meta file - regenerate
|
||||
await GenerateMetaFileAsync(file, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove orphaned entries from database (files that no longer exist)
|
||||
await RemoveOrphanedEntriesAsync(token);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to validate database: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the asset database manually.
|
||||
/// Scans the project directory for changes and processes any queued file system events.
|
||||
/// </summary>
|
||||
public static async Task<Result> RefreshAsync(CancellationToken token = default)
|
||||
{
|
||||
// Flush waiting commands to channel
|
||||
while (s_waitingCommands.TryDequeue(out var cmd))
|
||||
{
|
||||
s_commandChannel?.Writer.TryWrite(cmd);
|
||||
}
|
||||
|
||||
s_resetEventSlim.Reset();
|
||||
s_commandChannel?.Writer.TryWrite(new AssetCommand(AssetCommandType.ManualRefresh, string.Empty));
|
||||
s_commandProcessorTimer?.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
||||
|
||||
await Task.Run(s_resetEventSlim.Wait, token);
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark an asset as dirty (modified in memory but not yet saved).
|
||||
/// This state is NOT persisted and will be lost on application restart.
|
||||
/// </summary>
|
||||
public static void MarkDirty(Guid assetGuid)
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
s_dirtyAssets.Add(assetGuid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if an asset is marked as dirty.
|
||||
/// </summary>
|
||||
public static bool IsDirty(Guid assetGuid)
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
return s_dirtyAssets.Contains(assetGuid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all dirty assets.
|
||||
/// </summary>
|
||||
public static Guid[] GetDirtyAssets()
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
return s_dirtyAssets.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear dirty flag for an asset (typically after saving).
|
||||
/// </summary>
|
||||
public static void ClearDirty(Guid assetGuid)
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
s_dirtyAssets.Remove(assetGuid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all dirty flags.
|
||||
/// </summary>
|
||||
public static void ClearAllDirty()
|
||||
{
|
||||
lock (s_dbLock)
|
||||
{
|
||||
s_dirtyAssets.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable automatic asset database refresh.
|
||||
/// When disabled, file system events are queued and processed only when RefreshAsync() is called.
|
||||
/// </summary>
|
||||
public static void SetAutoRefresh(bool enabled)
|
||||
{
|
||||
s_autoRefreshEnabled = enabled;
|
||||
}
|
||||
|
||||
internal static void FlushPendingCommands()
|
||||
{
|
||||
// Stop timer temporarily
|
||||
s_commandProcessorTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
// Give a tiny bit of time for any in-flight file watcher events to post to channel
|
||||
Thread.Sleep(50);
|
||||
|
||||
// Process all commands now
|
||||
ProcessPendingCommands(null);
|
||||
}
|
||||
|
||||
private static async ValueTask PostCommandAsync(AssetCommand command, CancellationToken token = default)
|
||||
{
|
||||
if (s_commandChannel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_autoRefreshEnabled)
|
||||
{
|
||||
await s_commandChannel.Writer.WriteAsync(command, token);
|
||||
s_commandProcessorTimer?.Change(s_debounceDelay, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
s_waitingCommands.Enqueue(command);
|
||||
}
|
||||
}
|
||||
|
||||
private static async void ProcessPendingCommands(object? state)
|
||||
{
|
||||
if (s_commandChannel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// // Collect all pending commands
|
||||
// var commands = new List<AssetCommand>();
|
||||
//
|
||||
// while (s_commandChannel.Reader.TryRead(out var cmd))
|
||||
// {
|
||||
// commands.Add(cmd);
|
||||
// }
|
||||
|
||||
// // Group commands by path (last command wins)
|
||||
// var commandsByPath = new Dictionary<string, AssetCommand>();
|
||||
// foreach (var cmd in commands)
|
||||
// {
|
||||
// commandsByPath[cmd.Path] = cmd;
|
||||
// }
|
||||
|
||||
// NOTE: We handle the temp file filtering in each command handler now
|
||||
// We should able to remove this allocation heavy code
|
||||
|
||||
// Filter out temp files (files that were created then deleted)
|
||||
// lock (s_commandLock)
|
||||
// {
|
||||
// var pathsToProcess = commandsByPath.Keys.ToList();
|
||||
// foreach (var path in pathsToProcess)
|
||||
// {
|
||||
// // If file was created/modified but doesn't exist anymore, skip
|
||||
// if (!File.Exists(path) && commandsByPath[path].Type != AssetCommandType.FileDeleted)
|
||||
// {
|
||||
// commandsByPath.Remove(path);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Clear pending paths
|
||||
// s_pendingCommandPaths.Clear();
|
||||
// }
|
||||
|
||||
// Execute commands
|
||||
// NOTE: We many don't need to collect all commands first, just process as we read.
|
||||
// Channel in c# is thread-safe for multiple readers/writers.
|
||||
//await foreach (var cmd in s_commandChannel.Reader.ReadAllAsync())
|
||||
//{
|
||||
// await ExecuteCommandAsync(cmd);
|
||||
//}
|
||||
|
||||
while (s_commandChannel.Reader.TryRead(out var cmd))
|
||||
{
|
||||
await ExecuteCommandAsync(cmd);
|
||||
}
|
||||
|
||||
await ImportDirtyAssetsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error processing commands: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
s_resetEventSlim.Set();
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask ExecuteCommandAsync(AssetCommand command)
|
||||
{
|
||||
switch (command.Type)
|
||||
{
|
||||
case AssetCommandType.FileCreated:
|
||||
await HandleFileCreatedAsync(command.Path);
|
||||
break;
|
||||
|
||||
case AssetCommandType.FileModified:
|
||||
await HandleFileModifiedAsync(command.Path);
|
||||
break;
|
||||
|
||||
case AssetCommandType.FileDeleted:
|
||||
await HandleFileDeletedAsync(command.Path);
|
||||
break;
|
||||
|
||||
case AssetCommandType.FileRenamed:
|
||||
if (command.OldPath != null)
|
||||
{
|
||||
await HandleFileRenamedAsync(command.OldPath, command.Path);
|
||||
}
|
||||
break;
|
||||
|
||||
case AssetCommandType.ManualRefresh:
|
||||
await ValidateAndFixDatabaseAsync(CancellationToken.None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask HandleFileCreatedAsync(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await GenerateMetaFileAsync(path, CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async ValueTask HandleFileModifiedAsync(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file hash changed
|
||||
var metaResult = await ReadMetaFileAsync(path, CancellationToken.None);
|
||||
if (metaResult.IsFailure)
|
||||
{
|
||||
// No .gmeta file - treat this as a new file creation
|
||||
await HandleFileCreatedAsync(path);
|
||||
return;
|
||||
}
|
||||
|
||||
var newHash = await CalculateFileHashAsync(path, CancellationToken.None);
|
||||
var oldHash = await GetFileHashAsync(metaResult.Value.Guid, CancellationToken.None);
|
||||
|
||||
if (oldHash != newHash)
|
||||
{
|
||||
// File changed - update database and mark as dirty
|
||||
await UpsertAssetAsync(path, metaResult.Value, newHash, null, CancellationToken.None);
|
||||
MarkDirty(metaResult.Value.Guid);
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask HandleFileDeletedAsync(string path)
|
||||
{
|
||||
var metaFileResult = GetMetaFilePath(path);
|
||||
if (metaFileResult.IsSuccess && File.Exists(metaFileResult.Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
var metaResult = await ReadMetaFileAsync(path, CancellationToken.None);
|
||||
if (metaResult.IsSuccess)
|
||||
{
|
||||
var meta = metaResult.Value;
|
||||
|
||||
// Remove from database
|
||||
await RemoveAssetFromDatabaseAsync(meta.Guid, CancellationToken.None);
|
||||
|
||||
// Mark dependent assets as dirty
|
||||
await MarkDependentAssetsDirtyAsync(meta.Guid);
|
||||
}
|
||||
|
||||
File.Delete(metaFileResult.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error deleting asset metadata: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async ValueTask HandleFileRenamedAsync(string oldPath, string newPath)
|
||||
{
|
||||
var oldMetaPath = oldPath + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
var newMetaPath = newPath + Utilities.FileExtensions.META_FILE_EXTENSION;
|
||||
|
||||
if (File.Exists(newMetaPath))
|
||||
{
|
||||
// Validate and update
|
||||
await GenerateMetaFileAsync(newPath, CancellationToken.None);
|
||||
}
|
||||
else if (File.Exists(oldMetaPath))
|
||||
{
|
||||
// Move meta file
|
||||
File.Move(oldMetaPath, newMetaPath);
|
||||
|
||||
// Update database with new path and recalculated hash
|
||||
var metaResult = await ReadMetaFileAsync(newPath, CancellationToken.None);
|
||||
if (metaResult.IsSuccess)
|
||||
{
|
||||
var fileHash = await CalculateFileHashAsync(newPath, CancellationToken.None);
|
||||
await UpsertAssetAsync(newPath, metaResult.Value, fileHash, null, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generate new meta file
|
||||
await GenerateMetaFileAsync(newPath, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Delete old meta if it still exists
|
||||
if (File.Exists(oldMetaPath) && oldMetaPath != newMetaPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(oldMetaPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Shutdown()
|
||||
{
|
||||
lock (s_initializationLock)
|
||||
{
|
||||
if (!s_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
s_watcher?.Dispose();
|
||||
s_watcher = null;
|
||||
|
||||
s_commandProcessorTimer?.Dispose();
|
||||
s_commandProcessorTimer = null;
|
||||
|
||||
s_dbConnection?.Close();
|
||||
s_dbConnection?.Dispose();
|
||||
s_dbConnection = null;
|
||||
|
||||
s_assetPathLookup.Clear();
|
||||
s_pathAssetLookup.Clear();
|
||||
s_dirtyAssets.Clear();
|
||||
s_waitingCommands.Clear();
|
||||
s_importerInstances.Clear();
|
||||
s_importerTypeLookup.Clear();
|
||||
|
||||
s_initialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
# Asset Database Architecture
|
||||
|
||||
This document details the architectural design and data flow of the `AssetHandle` module in Ghost Editor.
|
||||
|
||||
## System Overview
|
||||
|
||||
The Asset Database acts as the bridge between the raw file system (Source Assets) and the runtime engine (Imported Assets). It maintains a consistent state using a dual-storage approach:
|
||||
1. **File System**: The source of truth. Contains source files (e.g., `.png`, `.fbx`) and metadata files (`.gmeta`).
|
||||
2. **SQLite Database**: An acceleration layer (cache) for fast lookups, dependency tracking, and searching.
|
||||
|
||||
## Data Flow
|
||||
|
||||
### 1. Asset Discovery & Registration
|
||||
When the editor starts or a file changes:
|
||||
1. **FileSystemWatcher** detects the change (Create/Delete/Modify/Rename).
|
||||
2. **Event Handler** queues an `AssetCommand` (debounce mechanism prevents event storms).
|
||||
3. **Command Processor** executes the command:
|
||||
* **New File**: Generates a `.gmeta` file with a new GUID and default settings. Adds to SQLite.
|
||||
* **Modified File**: Checks hash. If changed, marks asset as "Dirty" and updates SQLite.
|
||||
* **Deleted File**: Removes from SQLite and marks dependents as "Dirty".
|
||||
|
||||
### 2. Import Pipeline
|
||||
The import process converts source formats into engine-ready data.
|
||||
|
||||
**Flow:**
|
||||
1. `AssetDatabase.ImportDirtyAssetsAsync()` or direct `ImportAssetAsync` is called.
|
||||
2. System looks up the registered `AssetImporter` for the file extension.
|
||||
3. `AssetImporter.ImportAsync` is invoked with the source path and metadata.
|
||||
4. Importer reads source file and settings from metadata.
|
||||
5. Importer processes data (e.g., compiles shaders, compresses textures).
|
||||
6. Importer calls `AssetDatabase.SaveImportedAsset(guid, data)`.
|
||||
7. Data is serialized to JSON (or binary) in the `Cache/ImportedAssets` directory as `{GUID}.asset`.
|
||||
|
||||
### 3. Loading Pipeline
|
||||
When the engine requests an asset:
|
||||
|
||||
**Flow:**
|
||||
1. `AssetDatabase.LoadAsset<T>(guid)` is called.
|
||||
2. **Memory Cache Check**:
|
||||
* Checks `s_assetCache` (ConcurrentDictionary).
|
||||
* If found: Updates LRU timestamp and returns object.
|
||||
* If not found: Proceeds to disk load.
|
||||
3. **Disk Load**:
|
||||
* Locates `{GUID}.asset` in `Cache/ImportedAssets`.
|
||||
* Deserializes the data into the target runtime type (e.g., `TextureAsset`).
|
||||
4. **Cache Update**:
|
||||
* Adds new object to `s_assetCache`.
|
||||
* If cache size > `MAX_CACHED_ASSETS` (1000), evicts oldest 20% based on access time.
|
||||
|
||||
## Key Components Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[Editor / User] -->|File Ops| API[AssetDatabase API]
|
||||
FS[File System] -->|Events| Watcher[FileSystemWatcher]
|
||||
|
||||
subgraph AssetDatabase
|
||||
API --> DB[SQLite Database]
|
||||
API --> Meta[Meta Handler]
|
||||
API --> Loader[Asset Loader]
|
||||
API --> Importer[Import System]
|
||||
|
||||
Watcher -->|Queue| Cmd[Command Processor]
|
||||
Cmd --> Meta
|
||||
Cmd --> DB
|
||||
|
||||
Importer -->|Read| FS
|
||||
Importer -->|Write| Cache[Imported Assets Cache]
|
||||
|
||||
Loader -->|Read| Cache
|
||||
Loader -->|Check| MemCache[Memory LRU Cache]
|
||||
end
|
||||
|
||||
Meta -->|Read/Write| FS
|
||||
DB -->|Index| FS
|
||||
```
|
||||
|
||||
## Database Schema (SQLite)
|
||||
|
||||
The `AssetDatabase.db` contains a single `Assets` table:
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| **Guid** | TEXT (PK) | The unique identifier of the asset. |
|
||||
| **Path** | TEXT | Relative path from `Assets/` folder. Indexed for fast lookup. |
|
||||
| **Version** | INTEGER | Importer version for migration support. |
|
||||
| **Tags** | TEXT | JSON array of string tags. |
|
||||
| **FileHash** | TEXT | SHA256 hash of the source file content. |
|
||||
| **DependencyGuids** | TEXT | JSON array of GUIDs this asset depends on. |
|
||||
| **LastModified** | INTEGER | Unix timestamp of last modification. |
|
||||
|
||||
## Detailed Subsystems
|
||||
|
||||
### Metadata System (`.gmeta`)
|
||||
* **Format**: JSON.
|
||||
* **Content**: GUID, Version, Tags, ImporterSettings (per importer type).
|
||||
* **Strategy**: The `.gmeta` file is the *only* place the persistent GUID lives. If the database is corrupted, it can be rebuilt entirely by scanning the file system and reading `.gmeta` files.
|
||||
|
||||
### Threading & Safety
|
||||
* **Locks**:
|
||||
* `s_dbLock`: Protects in-memory dictionaries (`s_assetPathLookup`) and dirty tracking.
|
||||
* `s_commandLock`: Protects the command queue for file events.
|
||||
* **Async**: Heavy I/O operations (DB access, File I/O) are async.
|
||||
* **Channels**: Uses `System.Threading.Channels` to decouple high-frequency file system events from database processing.
|
||||
|
||||
### Importer Registry
|
||||
* Uses `TypeCache` and reflection to find classes with `[AssetImporter]`.
|
||||
* Mappings are stored in `s_importerTypeLookup` (Extension -> Type).
|
||||
* Importers are stateless (instantiated on demand or cached as singletons depending on implementation, currently cached in `s_importerInstances`).
|
||||
|
||||
## Future Improvements / Known Limitations
|
||||
|
||||
1. **Binary Formats**: Currently, imported assets are stored as JSON. For large assets (textures, models), a binary format is required for performance.
|
||||
2. **Dependency Graph**: While dependencies are stored, a full graph traversal for complex invalidation (e.g., if A changes, re-import B which depends on A) is partial.
|
||||
3. **Cross-Process Locking**: SQLite is file-based; concurrent access from multiple editor instances needs careful file locking mode configuration.
|
||||
@@ -1,131 +0,0 @@
|
||||
# Asset Database Documentation
|
||||
|
||||
The Asset Database is a core component of the Ghost Editor responsible for managing the lifecycle, storage, import, and retrieval of project assets. It provides a unified API for interacting with assets, ensuring that metadata (GUIDs, tags, settings) stays synchronized with files on disk.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **GUID-based Asset Identification**: Every asset is uniquely identified by a stable GUID, stored in a sidecar `.gmeta` file.
|
||||
- **Automatic Importing**: Monitors the file system for changes and automatically imports assets using registered importers.
|
||||
- **Dependency Tracking**: Tracks dependencies between assets to ensure validity and trigger re-imports when dependencies change.
|
||||
- **Caching**: Implements an LRU (Least Recently Used) cache for loaded assets to optimize performance.
|
||||
- **SQLite Backed**: Uses a local SQLite database for fast lookups (Path <-> GUID) and metadata queries.
|
||||
- **Metadata Management**: Handles `.gmeta` files automatically, including generation, validation, and cleanup.
|
||||
|
||||
## usage
|
||||
|
||||
### Initialization
|
||||
The Asset Database must be initialized after the project is loaded.
|
||||
```csharp
|
||||
await AssetDatabase.Initialize(cancellationToken);
|
||||
```
|
||||
|
||||
### Loading Assets
|
||||
Assets can be loaded by GUID or by Path.
|
||||
|
||||
```csharp
|
||||
// Load by Path
|
||||
var result = AssetDatabase.LoadAssetAtPath<TextureAsset>("Assets/Textures/my_texture.png");
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var texture = result.Value;
|
||||
}
|
||||
|
||||
// Load by GUID
|
||||
var guid = ...;
|
||||
var result = AssetDatabase.LoadAsset<TextureAsset>(guid);
|
||||
```
|
||||
|
||||
### File Operations
|
||||
Always use the `AssetDatabase` API for file operations to ensure metadata is preserved.
|
||||
|
||||
```csharp
|
||||
// Create
|
||||
await AssetDatabase.CreateAssetAsync("Assets/Data/config.json", dataBytes);
|
||||
|
||||
// Move
|
||||
await AssetDatabase.MoveAssetAsync("Assets/Old/file.txt", "Assets/New/file.txt");
|
||||
|
||||
// Copy
|
||||
await AssetDatabase.CopyAssetAsync("Assets/template.txt", "Assets/instance.txt");
|
||||
|
||||
// Delete
|
||||
await AssetDatabase.DeleteAssetAsync("Assets/garbage.tmp");
|
||||
```
|
||||
|
||||
### Searching
|
||||
Find assets using wildcards or tags.
|
||||
|
||||
```csharp
|
||||
// Find all PNGs
|
||||
var guids = await AssetDatabase.FindAssetsByNameAsync("*.png");
|
||||
|
||||
// Find assets with a specific tag
|
||||
var enemyAssets = await AssetDatabase.FindAssetsByTagAsync("Enemy");
|
||||
```
|
||||
|
||||
### Tags
|
||||
Manage asset tags for organization.
|
||||
|
||||
```csharp
|
||||
// Get tags
|
||||
var tagsResult = await AssetDatabase.GetAssetTagsAsync(guid);
|
||||
|
||||
// Set tags
|
||||
await AssetDatabase.SetAssetTagsAsync(guid, new List<string> { "Level1", "Prop" });
|
||||
```
|
||||
|
||||
### Opening Assets
|
||||
Open an asset using its registered handler or the system default.
|
||||
```csharp
|
||||
AssetDatabase.OpenAsset("Assets/Docs/readme.txt");
|
||||
```
|
||||
|
||||
## Extending the Asset Database
|
||||
|
||||
### Creating a New Importer
|
||||
To support a new file type, create a class that inherits from `AssetImporter<T>` and decorate it with the `[AssetImporter]` attribute.
|
||||
|
||||
```csharp
|
||||
[AssetImporter(".myfmt")]
|
||||
internal class MyFormatImporter : AssetImporter<MyFormatSettings>
|
||||
{
|
||||
public override async Task<Result> ImportAsync(string assetPath, AssetMeta meta)
|
||||
{
|
||||
var settings = GetSettings(meta);
|
||||
|
||||
// 1. Read source file
|
||||
// 2. Process data
|
||||
// 3. Save imported data using AssetDatabase.SaveImportedAsset
|
||||
|
||||
var myAsset = new MyAsset(meta.Guid) { ... };
|
||||
return AssetDatabase.SaveImportedAsset(meta.Guid, myAsset);
|
||||
}
|
||||
}
|
||||
|
||||
internal class MyFormatSettings : ImporterSettings
|
||||
{
|
||||
public float Scale { get; set; } = 1.0f;
|
||||
}
|
||||
```
|
||||
|
||||
### Creating an Open Handler
|
||||
To define custom behavior when an asset is opened (e.g., double-clicked in the editor), use the `[AssetOpenHandler]` attribute.
|
||||
|
||||
```csharp
|
||||
internal static class MyHandlers
|
||||
{
|
||||
[AssetOpenHandler(".myfmt")]
|
||||
private static void OpenMyFormat(string path)
|
||||
{
|
||||
// Open custom editor window
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Internal Architecture
|
||||
|
||||
- **AssetDatabase.cs**: Core initialization and event coordination.
|
||||
- **AssetDatabase.SQLite.cs**: Database table management and queries.
|
||||
- **AssetDatabase.Meta.cs**: `.gmeta` file handling and file system watcher events.
|
||||
- **AssetDatabase.Importer.cs**: Importer discovery and execution.
|
||||
- **AssetDatabase.Loader.cs**: Asset loading and caching logic.
|
||||
@@ -1,81 +0,0 @@
|
||||
using Ghost.Core;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public abstract class AssetImporter
|
||||
{
|
||||
/// <summary>
|
||||
/// Import the asset at the specified path with the given settings.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Full path to the source asset file.</param>
|
||||
/// <param name="meta">Metadata for the asset.</param>
|
||||
/// <param name="token">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public abstract ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Export in-memory asset data to disk.
|
||||
/// Override this method to support creating assets from code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of asset data to export.</typeparam>
|
||||
/// <param name="assetPath">Full path where the asset should be saved.</param>
|
||||
/// <param name="assetData">In-memory asset data to serialize.</param>
|
||||
/// <param name="meta">Metadata for the asset.</param>
|
||||
/// <param name="token">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure.</returns>
|
||||
public virtual ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
|
||||
where T : class
|
||||
{
|
||||
return ValueTask.FromResult(Result.Failure("This importer does not support exporting assets."));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate dependencies referenced by this asset.
|
||||
/// Dependencies are extracted from asset content during import and stored in the database.
|
||||
/// </summary>
|
||||
/// <param name="dependencies">List of dependency GUIDs extracted from the asset.</param>
|
||||
/// <returns>Result indicating if all dependencies are valid.</returns>
|
||||
protected virtual ValueTask<Result> ValidateDependenciesAsync(List<Guid> dependencies, CancellationToken token = default)
|
||||
{
|
||||
foreach (var dependencyGuid in dependencies)
|
||||
{
|
||||
var path = AssetDatabase.GuidToPath(dependencyGuid);
|
||||
if (path.IsFailure)
|
||||
{
|
||||
return ValueTask.FromResult(Result.Failure($"Missing dependency: {dependencyGuid}"));
|
||||
}
|
||||
|
||||
if (!File.Exists(path.Value))
|
||||
{
|
||||
return ValueTask.FromResult(Result.Failure($"Dependency file does not exist: {path.Value}"));
|
||||
}
|
||||
}
|
||||
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class AssetImporter<TSettings> : AssetImporter
|
||||
where TSettings : ImporterSettings, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the settings for this importer from the metadata.
|
||||
/// Creates default settings if none exist.
|
||||
/// </summary>
|
||||
/// <param name="meta">Asset metadata.</param>
|
||||
/// <returns>The importer settings.</returns>
|
||||
protected TSettings GetSettings(AssetMeta meta)
|
||||
{
|
||||
var typeName = GetType().Name;
|
||||
var settings = meta.GetImporterSettings<TSettings>(typeName);
|
||||
|
||||
if (settings != null)
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
var defaultSettings = new TSettings();
|
||||
meta.SetImporterSettings(typeName, defaultSettings);
|
||||
return defaultSettings;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||
internal class AssetImporterAttribute : Attribute
|
||||
{
|
||||
public string[] SupportedExtensions
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public AssetImporterAttribute(params string[] supportedExtensions)
|
||||
{
|
||||
SupportedExtensions = supportedExtensions;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for an asset, stored in .gmeta files.
|
||||
/// Contains GUID, version, tags, and importer settings.
|
||||
/// FileHash and Dependencies are stored in the database only, not in .gmeta files.
|
||||
/// </summary>
|
||||
public class AssetMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for the asset.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Guid")]
|
||||
public Guid Guid
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Version of the asset pipeline (not the asset itself).
|
||||
/// Used for migration when the asset pipeline is redesigned.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Version")]
|
||||
public int Version
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Tags for categorizing and searching assets.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Tags")]
|
||||
public List<string> Tags
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
|
||||
/// <summary>
|
||||
/// Importer settings specific to this asset.
|
||||
/// The key is the importer type name, and the value is a JSON element containing the settings.
|
||||
/// Use GetImporterSettings<T>() and SetImporterSettings<T>() to work with strongly-typed settings.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ImporterSettings")]
|
||||
public Dictionary<string, JsonElement> ImporterSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
|
||||
/// <summary>
|
||||
/// Get importer settings of a specific type.
|
||||
/// </summary>
|
||||
public T? GetImporterSettings<T>(string importerName) where T : ImporterSettings
|
||||
{
|
||||
if (ImporterSettings.TryGetValue(importerName, out var element))
|
||||
{
|
||||
return element.Deserialize<T>();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set importer settings.
|
||||
/// </summary>
|
||||
public void SetImporterSettings<T>(string importerName, T settings) where T : ImporterSettings
|
||||
{
|
||||
var element = JsonSerializer.SerializeToElement(settings);
|
||||
ImporterSettings[importerName] = element;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set importer settings (non-generic overload).
|
||||
/// </summary>
|
||||
internal void SetImporterSettings(string importerName, ImporterSettings settings)
|
||||
{
|
||||
var element = JsonSerializer.SerializeToElement(settings, settings.GetType());
|
||||
ImporterSettings[importerName] = element;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class AssetOpenHandlerAttribute : Attribute
|
||||
{
|
||||
public string[] Extensions
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public AssetOpenHandlerAttribute(params string[] extensions)
|
||||
{
|
||||
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
public abstract class ImporterSettings
|
||||
{
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using Ghost.Core;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle.Importers;
|
||||
|
||||
/// <summary>
|
||||
/// Example importer settings for text assets.
|
||||
/// </summary>
|
||||
internal class TextImporterSettings : ImporterSettings
|
||||
{
|
||||
public string Encoding
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = "UTF-8";
|
||||
|
||||
public bool TrimWhitespace
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example importer for text files (.txt, .md).
|
||||
/// This is a simple test importer to demonstrate the asset import system.
|
||||
/// </summary>
|
||||
[AssetImporter(".txt", ".md")]
|
||||
internal class TextImporter : AssetImporter<TextImporterSettings>
|
||||
{
|
||||
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
|
||||
{
|
||||
var settings = GetSettings(meta);
|
||||
|
||||
// Text files typically don't have dependencies
|
||||
// If they did, you would extract them from the content here
|
||||
var dependencies = new List<Guid>();
|
||||
|
||||
// Validate dependencies
|
||||
var depResult = await ValidateDependenciesAsync(dependencies);
|
||||
if (depResult.IsFailure)
|
||||
{
|
||||
return depResult;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read the file
|
||||
var content = await File.ReadAllTextAsync(assetPath, token);
|
||||
|
||||
if (settings.TrimWhitespace)
|
||||
{
|
||||
content = content.Trim();
|
||||
}
|
||||
|
||||
// TODO: Process the text content
|
||||
// For example:
|
||||
// - Convert to a specific format
|
||||
// - Extract metadata
|
||||
// - Generate assets
|
||||
// - Save to output folder
|
||||
|
||||
// For now, just report success
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to import text asset: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Ghost.Editor.Core.AssetHandle.Importers;
|
||||
|
||||
/// <summary>
|
||||
/// Importer settings for texture assets.
|
||||
/// </summary>
|
||||
internal class TextureImporterSettings : ImporterSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to generate mipmaps for the texture.
|
||||
/// </summary>
|
||||
public bool GenerateMipmaps
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the texture uses sRGB color space.
|
||||
/// </summary>
|
||||
public bool SRGB
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum texture size. Images larger than this will be downscaled.
|
||||
/// </summary>
|
||||
public uint MaxSize
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = 2048;
|
||||
|
||||
/// <summary>
|
||||
/// Texture compression format.
|
||||
/// Options: "None", "BC1", "BC3", "BC7"
|
||||
/// </summary>
|
||||
public string CompressionFormat
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = "None";
|
||||
|
||||
/// <summary>
|
||||
/// Texture filter mode.
|
||||
/// Options: "Point", "Bilinear", "Trilinear"
|
||||
/// </summary>
|
||||
public string FilterMode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = "Bilinear";
|
||||
|
||||
/// <summary>
|
||||
/// Texture wrap mode.
|
||||
/// Options: "Repeat", "Clamp", "Mirror"
|
||||
/// </summary>
|
||||
public string WrapMode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = "Repeat";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Importer for texture files (.png, .jpg, .jpeg, .dds, .tga, .bmp).
|
||||
/// Processes image files and converts them into engine-ready texture assets.
|
||||
/// </summary>
|
||||
[AssetImporter(".png", ".jpg", ".jpeg", ".dds", ".tga", ".bmp")]
|
||||
internal class TextureImporter : AssetImporter<TextureImporterSettings>
|
||||
{
|
||||
public override async ValueTask<Result> ImportAsync(string assetPath, AssetMeta meta, CancellationToken token = default)
|
||||
{
|
||||
var settings = GetSettings(meta);
|
||||
|
||||
// Textures typically don't reference other assets as dependencies
|
||||
// If they did (e.g., normal maps referencing base textures), extract here
|
||||
var dependencies = new List<Guid>();
|
||||
|
||||
// Validate dependencies
|
||||
var depResult = await ValidateDependenciesAsync(dependencies, token);
|
||||
if (depResult.IsFailure)
|
||||
{
|
||||
return depResult;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check if file exists
|
||||
if (!File.Exists(assetPath))
|
||||
{
|
||||
return Result.Failure($"Source texture file not found: {assetPath}");
|
||||
}
|
||||
|
||||
// Get image dimensions (simplified - in real implementation would use image library)
|
||||
var (width, height) = GetImageDimensions(assetPath);
|
||||
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return Result.Failure("Failed to read image dimensions");
|
||||
}
|
||||
|
||||
// Apply max size constraint
|
||||
if (width > settings.MaxSize || height > settings.MaxSize)
|
||||
{
|
||||
var scale = Math.Min(settings.MaxSize / (float)width, settings.MaxSize / (float)height);
|
||||
width = (uint)(width * scale);
|
||||
height = (uint)(height * scale);
|
||||
}
|
||||
|
||||
// Calculate mipmap count
|
||||
uint mipLevels = 1;
|
||||
if (settings.GenerateMipmaps)
|
||||
{
|
||||
mipLevels = CalculateMipLevels(width, height);
|
||||
}
|
||||
|
||||
// Determine format
|
||||
var format = settings.CompressionFormat == "None" ? "RGBA8" : settings.CompressionFormat;
|
||||
|
||||
// Create texture asset
|
||||
var textureAsset = new TextureAsset(meta.Guid, Path.GetFileNameWithoutExtension(assetPath))
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
MipLevels = mipLevels,
|
||||
Format = format,
|
||||
IsSRGB = settings.SRGB,
|
||||
SourcePath = assetPath
|
||||
};
|
||||
|
||||
// Save the imported asset data
|
||||
var saveResult = AssetDatabase.SaveImportedAsset(meta.Guid, textureAsset);
|
||||
if (saveResult.IsFailure)
|
||||
{
|
||||
return Result.Failure($"Failed to save texture asset: {saveResult.Message}");
|
||||
}
|
||||
|
||||
// In a real implementation, you would:
|
||||
// 1. Load the image using a library like ImageSharp or StbImageSharp
|
||||
// 2. Resize if needed
|
||||
// 3. Generate mipmaps
|
||||
// 4. Compress if needed
|
||||
// 5. Save the processed texture data to the ImportedAssets folder
|
||||
// 6. Update the hash in database
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to import texture: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get image dimensions from file.
|
||||
/// Simplified implementation - in production, use an image library.
|
||||
/// </summary>
|
||||
private static (uint width, uint height) GetImageDimensions(string imagePath)
|
||||
{
|
||||
// This is a placeholder implementation
|
||||
// In a real implementation, you would use a library like:
|
||||
// - ImageSharp
|
||||
// - StbImageSharp
|
||||
// - DirectXTex (for DDS files)
|
||||
|
||||
var extension = Path.GetExtension(imagePath).ToLowerInvariant();
|
||||
|
||||
if (extension == ".dds")
|
||||
{
|
||||
// For DDS files, read the header
|
||||
// DDS header format: https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header
|
||||
return ReadDDSHeader(imagePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For PNG/JPG/etc, we would use an image library
|
||||
// For now, return placeholder values
|
||||
return (1024, 1024);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read DDS file header to get dimensions.
|
||||
/// </summary>
|
||||
private static (uint width, uint height) ReadDDSHeader(string ddsPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(ddsPath);
|
||||
using var reader = new BinaryReader(stream);
|
||||
|
||||
// Read magic number (should be "DDS ")
|
||||
var magic = reader.ReadUInt32();
|
||||
if (magic != 0x20534444) // "DDS " in little-endian
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Read header size (should be 124)
|
||||
var headerSize = reader.ReadUInt32();
|
||||
if (headerSize != 124)
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Skip flags
|
||||
reader.ReadUInt32();
|
||||
|
||||
// Read height and width
|
||||
var height = reader.ReadUInt32();
|
||||
var width = reader.ReadUInt32();
|
||||
|
||||
return (width, height);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Export a texture asset from memory to disk.
|
||||
/// </summary>
|
||||
public override async ValueTask<Result> ExportAsync<T>(string assetPath, T assetData, AssetMeta meta, CancellationToken token = default)
|
||||
{
|
||||
if (assetData is not TextureAsset textureAsset)
|
||||
{
|
||||
return Result.Failure($"Asset data is not a TextureAsset, got {typeof(T).Name}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// In a real implementation, you would:
|
||||
// 1. Convert the texture data to the appropriate format
|
||||
// 2. Write the image file (PNG, DDS, etc.)
|
||||
// 3. Save metadata
|
||||
|
||||
// For now, just save metadata as JSON
|
||||
var json = JsonSerializer.Serialize(textureAsset, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
await File.WriteAllTextAsync(assetPath, json, token);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure($"Failed to export texture: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate number of mipmap levels for a given texture size.
|
||||
/// </summary>
|
||||
private static uint CalculateMipLevels(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint count = 1;
|
||||
while (width > 1 || height > 1)
|
||||
{
|
||||
width >>= 1;
|
||||
height >>= 1;
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
/// <summary>
|
||||
/// The base class for all asset types in the Ghost Editor.
|
||||
/// </summary>
|
||||
public abstract class Asset
|
||||
{
|
||||
public abstract string Name
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Guid ID
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
protected Asset(Guid id)
|
||||
{
|
||||
ID = id;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
namespace Ghost.Editor.Core.AssetHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a texture asset.
|
||||
/// </summary>
|
||||
public class TextureAsset : Asset
|
||||
{
|
||||
public override string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Width of the texture in pixels.
|
||||
/// </summary>
|
||||
public uint Width
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Height of the texture in pixels.
|
||||
/// </summary>
|
||||
public uint Height
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Number of mipmap levels.
|
||||
/// </summary>
|
||||
public uint MipLevels
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Texture format (e.g., "RGBA8", "BC1", "BC7").
|
||||
/// </summary>
|
||||
public string Format
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the texture uses sRGB color space.
|
||||
/// </summary>
|
||||
public bool IsSRGB
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Relative path to the source image file.
|
||||
/// </summary>
|
||||
public string SourcePath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public TextureAsset(Guid id, string name) : base(id)
|
||||
{
|
||||
Name = name;
|
||||
Format = "RGBA8";
|
||||
IsSRGB = true;
|
||||
SourcePath = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Ghost.Editor.Core.Contracts;
|
||||
|
||||
public interface INavigationAware
|
||||
{
|
||||
public void OnNavigatedTo(object? parameter);
|
||||
public void OnNavigatedFrom();
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Ghost.Editor.Controls.Internal" />
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Ghost.Editor.Core.Inspector;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class CustomEditorAttribute(Type targetType) : Attribute
|
||||
{
|
||||
internal Type TargetType
|
||||
{
|
||||
get;
|
||||
} = targetType;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Ghost.Editor.Core.Inspector;
|
||||
|
||||
public interface IInspectable
|
||||
{
|
||||
public IconSource? CreateIcon();
|
||||
|
||||
public UIElement? CreateHeader();
|
||||
|
||||
public UIElement? CreateInspector();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Ghost.Editor.Core.Inspector;
|
||||
|
||||
internal interface IInspectorService
|
||||
{
|
||||
public IInspectable? SelectedInspectable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public event Action? OnSelectionChanged;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace Ghost.Editor.Core.Inspector;
|
||||
|
||||
public class InspectorService : IInspectorService
|
||||
{
|
||||
public IInspectable? SelectedInspectable
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field != value)
|
||||
{
|
||||
field = value;
|
||||
OnSelectionChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? OnSelectionChanged;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
|
||||
namespace Ghost.Editor.Core.Notifications;
|
||||
|
||||
public interface INotificationService
|
||||
{
|
||||
public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null);
|
||||
public void ShowNotification(Notification notification);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Ghost.Editor.Core.Progress;
|
||||
|
||||
public interface IProgressService
|
||||
{
|
||||
public void ShowProgress(string message, double progress = 0.0);
|
||||
public void ShowIndeterminateProgress(string message);
|
||||
public void SetProgress(double progress);
|
||||
public void HideProgress();
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Ghost.Editor.Core.Utilities;
|
||||
|
||||
public static class EditorApplication
|
||||
{
|
||||
private static IServiceProvider? _serviceProvider;
|
||||
|
||||
public static Application Current => Application.Current;
|
||||
|
||||
internal static void Initialize(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public static T GetService<T>()
|
||||
where T : class
|
||||
{
|
||||
if (_serviceProvider?.GetService(typeof(T)) is not T service)
|
||||
{
|
||||
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using Ghost.Core.Attributes;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ghost.Editor.Core.Utilities;
|
||||
|
||||
public static class TypeCache
|
||||
{
|
||||
private static readonly TypeInfo[] s_types;
|
||||
|
||||
static TypeCache()
|
||||
{
|
||||
var loadableTypes = new List<Type>(512);
|
||||
var assembliesToScan = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => a.GetCustomAttribute<EngineAssemblyAttribute>() != null);
|
||||
|
||||
foreach (var assembly in assembliesToScan)
|
||||
{
|
||||
try
|
||||
{
|
||||
loadableTypes.AddRange(assembly.GetTypes());
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
var types = ex.Types.Where(t => t != null);
|
||||
loadableTypes.AddRange(types!);
|
||||
}
|
||||
}
|
||||
|
||||
s_types = loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
|
||||
}
|
||||
|
||||
public static Type[] GetTypes()
|
||||
{
|
||||
return s_types;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Ghost.Data.Resources;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.Core.Utilities;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Ghost.Editor;
|
||||
|
||||
internal static class ActivationHandler
|
||||
{
|
||||
private static void FolderInitialization()
|
||||
{
|
||||
if (!Directory.Exists(DataPath.s_applicationDataFolder))
|
||||
{
|
||||
Directory.CreateDirectory(DataPath.s_applicationDataFolder);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(DataPath.s_projectTemplateFolder))
|
||||
{
|
||||
Directory.CreateDirectory(DataPath.s_projectTemplateFolder);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Handle(LaunchActivatedEventArgs args)
|
||||
{
|
||||
FolderInitialization();
|
||||
ProjectService.EnsureDefaultTemplate();
|
||||
|
||||
EditorApplication.Initialize(((App)(Application.Current)).Host.Services);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.Core.AppState;
|
||||
using Ghost.Editor.Core.Inspector;
|
||||
using Ghost.Editor.Core.Notifications;
|
||||
using Ghost.Editor.Core.Progress;
|
||||
using Ghost.Editor.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Ghost.Editor;
|
||||
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _window;
|
||||
|
||||
internal static Window? Window
|
||||
{
|
||||
get => (Current as App)!._window;
|
||||
set
|
||||
{
|
||||
if (Current is App app)
|
||||
{
|
||||
// HACK: As far as I can tell, there is no proper application shutdown event in WinUI 3.
|
||||
app._window?.Closed -= app.OnClosed;
|
||||
app._window = value;
|
||||
app._window?.Closed += app.OnClosed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal IHost Host
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
internal App()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Host = Microsoft.Extensions.Hosting.Host.
|
||||
CreateDefaultBuilder().
|
||||
UseContentRoot(AppContext.BaseDirectory).
|
||||
ConfigureServices((context, services) =>
|
||||
{
|
||||
HostHelper.AddLandingScope(context, services);
|
||||
HostHelper.AddEngineScope(context, services);
|
||||
|
||||
services.AddSingleton<AppStateMachine>();
|
||||
services.AddSingleton<INotificationService, NotificationService>();
|
||||
services.AddSingleton<IProgressService, ProgressService>();
|
||||
services.AddSingleton<IInspectorService, InspectorService>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
UnhandledException += App_UnhandledException;
|
||||
}
|
||||
|
||||
internal static IServiceScope CreateScope()
|
||||
{
|
||||
return (Current as App)!.Host.Services.CreateScope();
|
||||
}
|
||||
|
||||
public static T GetService<T>() where T : class
|
||||
{
|
||||
if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
|
||||
{
|
||||
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override async void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
base.OnLaunched(args);
|
||||
|
||||
await Host.StartAsync();
|
||||
ActivationHandler.Handle(args);
|
||||
|
||||
var stateMachine = GetService<AppStateMachine>();
|
||||
stateMachine.RegisterState(StateKey.Landing, () => new LandingState());
|
||||
stateMachine.RegisterState(StateKey.EngineEditor, () => new EditorState());
|
||||
|
||||
await stateMachine.TransitionToAsync(StateKey.Landing);
|
||||
}
|
||||
|
||||
private void OnClosed(object? sender, WindowEventArgs args)
|
||||
{
|
||||
Host.StopAsync().GetAwaiter().GetResult();
|
||||
Host.Dispose();
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError(e.Exception);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 583 B |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 852 B |
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,38 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Ghost.Editor.Controls;
|
||||
|
||||
public abstract partial class ViewModelPage<VM> : Page
|
||||
where VM : ObservableObject
|
||||
{
|
||||
public VM ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
protected ViewModelPage(VM viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
if (ViewModel is INavigationAware navigationAware)
|
||||
{
|
||||
navigationAware.OnNavigatedTo(e.Parameter);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedFrom(e);
|
||||
if (ViewModel is INavigationAware navigationAware)
|
||||
{
|
||||
navigationAware.OnNavigatedFrom();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.Core.AssetHandle;
|
||||
using Ghost.Editor.View.Windows;
|
||||
using Ghost.Engine;
|
||||
|
||||
namespace Ghost.Editor.Core.AppState;
|
||||
|
||||
internal class EditorState : IAppState
|
||||
{
|
||||
private EngineEditorWindow? _window;
|
||||
private EngineCore? _engineCore;
|
||||
|
||||
public ValueTask<Result> OnExitingAsync()
|
||||
{
|
||||
if (App.Window == _window)
|
||||
{
|
||||
App.Window = null;
|
||||
}
|
||||
|
||||
_engineCore?.Dispose();
|
||||
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
|
||||
public ValueTask<Result> OnEnteringAsync(object? parameter)
|
||||
{
|
||||
if (parameter is not ProjectMetadataInfo metadataInfo)
|
||||
{
|
||||
return ValueTask.FromResult(Result.Failure("Invalid parameter for entering EditorState."));
|
||||
}
|
||||
|
||||
ProjectService.CurrentProject = metadataInfo;
|
||||
|
||||
_engineCore = App.GetService<EngineCore>();
|
||||
_engineCore.Init();
|
||||
|
||||
_window = App.GetService<EngineEditorWindow>();
|
||||
_window.Activate();
|
||||
|
||||
App.Window = _window;
|
||||
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
|
||||
public ValueTask<Result> OnExitedAsync()
|
||||
{
|
||||
_window?.Close();
|
||||
_window = null;
|
||||
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
|
||||
public async ValueTask<Result> OnEnteredAsync(object? parameter)
|
||||
{
|
||||
await AssetDatabase.Initialize();
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Editor.View.Windows;
|
||||
|
||||
namespace Ghost.Editor.Core.AppState;
|
||||
|
||||
internal class LandingState : IAppState
|
||||
{
|
||||
private LandingWindow? _window;
|
||||
|
||||
public ValueTask<Result> OnExitingAsync()
|
||||
{
|
||||
if (App.Window == _window)
|
||||
{
|
||||
App.Window = null;
|
||||
}
|
||||
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
|
||||
public ValueTask<Result> OnEnteringAsync(object? parameter)
|
||||
{
|
||||
_window = App.GetService<LandingWindow>();
|
||||
_window.Activate();
|
||||
|
||||
App.Window = _window;
|
||||
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
|
||||
public ValueTask<Result> OnExitedAsync()
|
||||
{
|
||||
_window?.Close();
|
||||
_window = null;
|
||||
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
|
||||
public ValueTask<Result> OnEnteredAsync(object? parameter)
|
||||
{
|
||||
return ValueTask.FromResult(Result.Success());
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<!-- in .net 10, field keyword is not preview anymore, but we are still waiting roslyn team to update their code analyzer packages -->
|
||||
<langversion>preview</langversion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\StoreLogo.png" />
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
|
||||
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ghost.Editor.Core\Ghost.Editor.Core.csproj" />
|
||||
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\Landing\CreateProjectPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Window\Landing.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\Landing\OpenProjectPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\EngineEditor\InspectorPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\EngineEditor\HierarchyPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\EngineEditor\ProjectPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\EngineEditor\ConsolePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Themes\Override.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Windows\EngineEditorWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Pages\EngineEditor\ScenePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals" />
|
||||
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Publish Properties -->
|
||||
<PropertyGroup>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
<Nullable>enable</Nullable>
|
||||
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<PublishAot>False</PublishAot>
|
||||
<PublishTrimmed>False</PublishTrimmed>
|
||||
<RootNamespace>Ghost.Editor</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.Models;
|
||||
|
||||
internal class ExplorerItem(string name, string path, bool isDirectory)
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
} = name;
|
||||
|
||||
public string FullName
|
||||
{
|
||||
get;
|
||||
} = path;
|
||||
|
||||
public bool IsDirectory
|
||||
{
|
||||
get;
|
||||
} = isDirectory;
|
||||
|
||||
public ObservableCollection<ExplorerItem>? Children
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
|
||||
xmlns:internal="using:Ghost.Editor.Controls.Internal">
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<Style TargetType="internal:NavigationTabView">
|
||||
<Setter Property="TabWidthMode" Value="Compact" />
|
||||
</Style>
|
||||
<Style TargetType="NumberBox" />
|
||||
</ResourceDictionary>
|
||||
@@ -1,50 +0,0 @@
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.View.Pages.EngineEditor;
|
||||
using Ghost.Editor.View.Pages.Landing;
|
||||
using Ghost.Editor.View.Windows;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Ghost.Editor.ViewModels.Pages.Landing;
|
||||
using Ghost.Editor.ViewModels.Windows;
|
||||
using Ghost.Engine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Ghost.Editor.Utilities;
|
||||
|
||||
internal static partial class HostHelper
|
||||
{
|
||||
public static void AddLandingScope(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<LandingWindow>();
|
||||
|
||||
services.AddTransient<CreateProjectPage>();
|
||||
services.AddTransient<CreateProjectViewModel>();
|
||||
|
||||
services.AddTransient<OpenProjectPage>();
|
||||
services.AddTransient<OpenProjectViewModel>();
|
||||
|
||||
services.AddTransient<ProjectService>();
|
||||
}
|
||||
|
||||
public static void AddEngineScope(HostBuilderContext context, IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<EngineCore>();
|
||||
|
||||
services.AddSingleton<EngineEditorWindow>();
|
||||
services.AddSingleton<EngineEditorViewModel>();
|
||||
|
||||
services.AddTransient<ScenePage>();
|
||||
|
||||
services.AddTransient<HierarchyPage>();
|
||||
services.AddTransient<HierarchyViewModel>();
|
||||
|
||||
services.AddTransient<ProjectPage>();
|
||||
services.AddTransient<ProjectViewModel>();
|
||||
|
||||
services.AddTransient<ConsolePage>();
|
||||
services.AddTransient<ConsoleViewModel>();
|
||||
|
||||
services.AddTransient<InspectorPage>();
|
||||
services.AddTransient<InspectorViewModel>();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Ghost.Editor.View.Pages.EngineEditor.ConsolePage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<CommandBar DefaultLabelPosition="Collapsed">
|
||||
<CommandBar.PrimaryCommands>
|
||||
<AppBarButton Command="{x:Bind ViewModel.ClearLogsCommand}" Content="Clear" />
|
||||
<AppBarSeparator />
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowInfo, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowWarning, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
<AppBarToggleButton Width="45" IsChecked="{x:Bind ViewModel.ShowError, Mode=TwoWay}">
|
||||
<AppBarToggleButton.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
</CommandBar.PrimaryCommands>
|
||||
|
||||
<CommandBar.SecondaryCommands>
|
||||
<AppBarToggleButton BorderThickness="0" Label="Clear On Play" />
|
||||
<AppBarToggleButton
|
||||
BorderThickness="0"
|
||||
IsChecked="{x:Bind ViewModel.ShowStackTrace, Mode=TwoWay}"
|
||||
Label="Show Stack Trace" />
|
||||
</CommandBar.SecondaryCommands>
|
||||
</CommandBar>
|
||||
</Grid>
|
||||
|
||||
<!-- Log Content -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="100" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ListView
|
||||
x:Name="LogListView"
|
||||
Grid.Row="0"
|
||||
ItemsSource="{x:Bind ViewModel.Logs, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedLog, Mode=TwoWay}" />
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Padding="4"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock
|
||||
IsTextSelectionEnabled="True"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedLog.ToString(), Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,19 +0,0 @@
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class ConsolePage : Page
|
||||
{
|
||||
public ConsoleViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ConsolePage()
|
||||
{
|
||||
ViewModel = App.GetService<ConsoleViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<internal:NavigationTabPage
|
||||
x:Class="Ghost.Editor.View.Pages.EngineEditor.HierarchyPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:internal="using:Ghost.Editor.Controls.Internal"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:sg="using:Ghost.Editor.Core.SceneGraph"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<internal:NavigationTabPage.Resources>
|
||||
<DataTemplate x:Key="SceneTemplate" x:DataType="sg:SceneGraphNode">
|
||||
<TreeViewItem
|
||||
AutomationProperties.Name="{x:Bind Name, Mode=OneWay}"
|
||||
Background="{ThemeResource ControlSolidFillColorDefaultBrush}"
|
||||
IsExpanded="True"
|
||||
ItemsSource="{x:Bind Children, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock Margin="10,0" Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="EntityTemplate" x:DataType="sg:SceneGraphNode">
|
||||
<TreeViewItem AutomationProperties.Name="{x:Bind Name, Mode=OneWay}" ItemsSource="{x:Bind Children, Mode=OneWay}">
|
||||
<StackPanel Margin="10,0" Orientation="Horizontal">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock Margin="5,0,0,0" Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
</internal:NavigationTabPage.Resources>
|
||||
|
||||
<Grid Padding="4,6" Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<!--<TreeView ItemsSource="{x:Bind ViewModel.SceneList}" SelectionChanged="TreeView_SelectionChanged">
|
||||
<TreeView.ItemTemplateSelector>
|
||||
<local:HierarchyTemplateSector />
|
||||
</TreeView.ItemTemplateSelector>
|
||||
</TreeView>-->
|
||||
</Grid>
|
||||
</internal:NavigationTabPage>
|
||||
@@ -1,61 +0,0 @@
|
||||
using Ghost.Editor.Controls.Internal;
|
||||
using Ghost.Editor.Core.Inspector;
|
||||
using Ghost.Editor.Core.SceneGraph;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class HierarchyPage : NavigationTabPage
|
||||
{
|
||||
private readonly IInspectorService _inspectorService;
|
||||
|
||||
public HierarchyViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public HierarchyPage()
|
||||
{
|
||||
_inspectorService = App.GetService<IInspectorService>();
|
||||
ViewModel = App.GetService<HierarchyViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
ViewModel.OnNavigatedTo(parameter);
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom()
|
||||
{
|
||||
ViewModel.OnNavigatedFrom();
|
||||
}
|
||||
|
||||
private void TreeView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
|
||||
{
|
||||
if (args.AddedItems.Count > 0 && args.AddedItems[0] is IInspectable inspectable)
|
||||
{
|
||||
_inspectorService.SelectedInspectable = inspectable;
|
||||
}
|
||||
else
|
||||
{
|
||||
_inspectorService.SelectedInspectable = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class HierarchyTemplateSector : DataTemplateSelector
|
||||
{
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
if (item is not SceneGraphNode node)
|
||||
{
|
||||
return base.SelectTemplateCore(item);
|
||||
}
|
||||
|
||||
return node.GetSceneHierarchyTemplate();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<internal:NavigationTabPage
|
||||
x:Class="Ghost.Editor.View.Pages.EngineEditor.InspectorPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:internal="using:Ghost.Editor.Controls.Internal"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="75" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Padding="15,0,10,0"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!--<IconSourceElement
|
||||
Grid.Column="0"
|
||||
Margin="0,0,15,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IconSource="{x:Bind ViewModel.Inspectable.Icon, Mode=OneWay}" />-->
|
||||
<!--<ContentPresenter Grid.Column="1" Content="{x:Bind ViewModel.Inspectable.HeaderContent, Mode=OneWay}" />-->
|
||||
</Grid>
|
||||
|
||||
<!-- Content -->
|
||||
<Grid Grid.Row="1" Padding="0,0,0,0">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<!--<ContentPresenter Content="{x:Bind ViewModel.Inspectable.InspectorContent, Mode=OneWay}" />-->
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</internal:NavigationTabPage>
|
||||
@@ -1,29 +0,0 @@
|
||||
using Ghost.Editor.Controls.Internal;
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class InspectorPage : NavigationTabPage
|
||||
{
|
||||
public InspectorViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public InspectorPage()
|
||||
{
|
||||
ViewModel = App.GetService<InspectorViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
ViewModel.OnNavigatedTo(parameter);
|
||||
}
|
||||
|
||||
public override void OnNavigatedFrom()
|
||||
{
|
||||
ViewModel.OnNavigatedFrom();
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Ghost.Editor.View.Pages.EngineEditor.ProjectPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converter="using:Ghost.Editor.Utilities.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:model="using:Ghost.Editor.Models"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<converter:AssetPathToGlyphConverter x:Key="AssetPathToGlyphConverter" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Folder Tree View -->
|
||||
<Grid
|
||||
Grid.Column="0"
|
||||
Padding="4"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<TreeView
|
||||
x:Name="DirectoryTreeView"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{x:Bind ViewModel.SubDirectories}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedDirectory, Mode=TwoWay}">
|
||||
<TreeView.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ExplorerItem">
|
||||
<TreeViewItem ItemsSource="{x:Bind Children}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</TreeViewItem>
|
||||
</DataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
</Grid>
|
||||
|
||||
<!-- Files -->
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Padding="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<BreadcrumbBar Height="15" />
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer
|
||||
Grid.Row="1"
|
||||
Padding="8"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<GridView
|
||||
x:Name="AssetsGridView"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{x:Bind ViewModel.DirectoryAssets, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedAsset, Mode=TwoWay}">
|
||||
<GridView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
|
||||
<Setter Property="Margin" Value="2" />
|
||||
</Style>
|
||||
</GridView.ItemContainerStyle>
|
||||
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate x:DataType="model:ExplorerItem">
|
||||
<Grid
|
||||
Width="100"
|
||||
Height="100"
|
||||
Padding="8"
|
||||
DoubleTapped="GridViewItem_DoubleTapped"
|
||||
IsDoubleTapEnabled="True">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="0.25*" />
|
||||
</Grid.RowDefinitions>
|
||||
<FontIcon FontSize="42" Glyph="{x:Bind FullName, Converter={StaticResource AssetPathToGlyphConverter}}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="8,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Padding="4"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
HorizontalTextAlignment="Left"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedAsset.FullName, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,25 +0,0 @@
|
||||
using Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class ProjectPage : Page
|
||||
{
|
||||
public ProjectViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public ProjectPage()
|
||||
{
|
||||
ViewModel = App.GetService<ProjectViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void GridViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||
{
|
||||
ViewModel.OpenSelected();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<internal:NavigationTabPage
|
||||
x:Class="Ghost.Editor.View.Pages.EngineEditor.ScenePage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:internal="using:Ghost.Editor.Controls.Internal"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<SwapChainPanel
|
||||
x:Name="SwapChainPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</internal:NavigationTabPage>
|
||||
@@ -1,45 +0,0 @@
|
||||
using Ghost.Editor.Controls.Internal;
|
||||
//using Ghost.Graphics.Contracts;
|
||||
//using Microsoft.UI.Xaml;
|
||||
//using Microsoft.UI.Xaml.Controls;
|
||||
//using WinRT;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||
|
||||
internal sealed partial class ScenePage : NavigationTabPage
|
||||
{
|
||||
//private Renderer? _renderView;
|
||||
//private ISwapChainPanelNative _swapChainPanelNative;
|
||||
|
||||
public ScenePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
//SwapChainPanel.Loaded += SwapChainPanel_Loaded;
|
||||
//SwapChainPanel.Unloaded += SwapChainPanel_Unloaded;
|
||||
//SwapChainPanel.SizeChanged += SwapChainPanel_SizeChanged;
|
||||
}
|
||||
|
||||
//private void SwapChainPanel_Loaded(object sender, RoutedEventArgs e)
|
||||
//{
|
||||
// var guid = typeof(ISwapChainPanelNative.Interface).GUID;
|
||||
// ((IWinRTObject)SwapChainPanel).NativeObject.TryAs(guid, out var swapChainPanelNativeHandle);
|
||||
// _swapChainPanelNative = new ISwapChainPanelNative(swapChainPanelNativeHandle);
|
||||
|
||||
// _renderView = GraphicsPipeline.GraphicsDevice.CreateRenderer(new(_swapChainPanelNative, (uint)SwapChainPanel.ActualWidth, (uint)SwapChainPanel.ActualHeight));
|
||||
//}
|
||||
|
||||
//private void SwapChainPanel_Unloaded(object sender, RoutedEventArgs e)
|
||||
//{
|
||||
// _swapChainPanelNative.Dispose();
|
||||
// _renderView?.Dispose();
|
||||
//}
|
||||
|
||||
//private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
//{
|
||||
// if (e.NewSize.Width > 8.0 && e.NewSize.Height > 8.0)
|
||||
// {
|
||||
// _renderView?.RequestResize((uint)e.NewSize.Width, (uint)e.NewSize.Height);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Ghost.Editor.View.Pages.Landing.CreateProjectPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Ghost.Data.Models"
|
||||
xmlns:editor="using:Ghost.Editor.Core.Controls"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
NavigationCacheMode="Enabled"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Template Info -->
|
||||
<Grid Grid.Column="0" Width="300">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,24"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="Template" />
|
||||
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
ItemsSource="{x:Bind ViewModel.templates}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedTemplate, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:TemplateData">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ImageIcon
|
||||
Grid.Column="0"
|
||||
Width="24"
|
||||
Height="24">
|
||||
<ImageIcon.Source>
|
||||
<BitmapImage UriSource="{x:Bind GetIconURI()}" />
|
||||
</ImageIcon.Source>
|
||||
</ImageIcon>
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Info.Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Grid>
|
||||
|
||||
<!-- Project Info -->
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Margin="16,0,0,0"
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="300" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" CornerRadius="4">
|
||||
<Image VerticalAlignment="Center" Stretch="UniformToFill">
|
||||
<Image.Source>
|
||||
<BitmapImage UriSource="{x:Bind ViewModel.SelectedTemplate.Value.GetPreviewURI(), Mode=OneWay}" />
|
||||
</Image.Source>
|
||||
</Image>
|
||||
<Grid
|
||||
MaxHeight="100"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{ThemeResource ControlOnImageFillColorDefaultBrush}">
|
||||
<TextBlock
|
||||
Margin="16"
|
||||
VerticalAlignment="Bottom"
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Description, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="1" Margin="8,0">
|
||||
<TextBlock
|
||||
Margin="0,16,0,8"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Name, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,8,0,16"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="Project Settings" />
|
||||
|
||||
<editor:PropertyField Label="Name">
|
||||
<TextBox Text="{x:Bind ViewModel.ProjectName, Mode=TwoWay}" />
|
||||
</editor:PropertyField>
|
||||
<editor:PropertyField Label="Location">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
IsReadOnly="True"
|
||||
Text="{x:Bind ViewModel.ProjectLocation, Mode=TwoWay}" />
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{x:Bind ViewModel.SelectionProjectLocationCommand}">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</editor:PropertyField>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="2">
|
||||
<Button
|
||||
Width="150"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.CreateProjectCommand}"
|
||||
Content="Create"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,32 +0,0 @@
|
||||
using Ghost.Editor.ViewModels.Pages.Landing;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.Landing;
|
||||
|
||||
internal sealed partial class CreateProjectPage : Page
|
||||
{
|
||||
public CreateProjectViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public CreateProjectPage()
|
||||
{
|
||||
ViewModel = App.GetService<CreateProjectViewModel>();
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
ViewModel.OnNavigatedTo(e.Parameter);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedFrom(e);
|
||||
ViewModel.OnNavigatedFrom();
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Ghost.Editor.View.Pages.Landing.OpenProjectPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:Ghost.Editor.Utilities.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Ghost.Data.Models"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
NavigationCacheMode="Enabled"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<converters:GetDirectoryNameConverter x:Key="DirNameConverter" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid x:Name="MainContainer">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="16,4">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="Projects" />
|
||||
<AutoSuggestBox
|
||||
Width="300"
|
||||
HorizontalAlignment="Right"
|
||||
PlaceholderText="Search project by name"
|
||||
QueryIcon="Find" />
|
||||
</Grid>
|
||||
|
||||
<!-- Header for the ListView -->
|
||||
<Grid Grid.Row="1" Margin="28,16,45,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="200" />
|
||||
<ColumnDefinition Width="165" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="NAME" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="LAST OPEN" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="ENGINE VERSION" />
|
||||
</Grid>
|
||||
|
||||
<!-- Project ListView -->
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Padding="8"
|
||||
AllowDrop="True"
|
||||
DragEnter="ProjectContainer_DragEnter"
|
||||
DragLeave="ProjectContainer_DragLeave"
|
||||
DragOver="ProjectContainer_DragOver"
|
||||
Drop="ProjectContainer_Drop">
|
||||
<ListView
|
||||
Padding="4,8"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_ItemClick"
|
||||
ItemsSource="{x:Bind ViewModel.projects}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:ProjectMetadataInfo">
|
||||
<Grid Height="64" Padding="4,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="200" />
|
||||
<ColumnDefinition Width="100" />
|
||||
<ColumnDefinition Width="65" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0" VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="{x:Bind Metadata.Name}" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,4,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Path, Converter={StaticResource DirNameConverter}}" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="16,4"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Metadata.LastOpened}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="16,4"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Metadata.EngineVersion}" />
|
||||
<Button
|
||||
Grid.Column="3"
|
||||
HorizontalAlignment="Right"
|
||||
Background="Transparent"
|
||||
BorderThickness="0">
|
||||
<FontIcon Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<!-- Drag Visual -->
|
||||
<Grid
|
||||
x:Name="DragVisual"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource ControlStrongStrokeColorDefaultBrush}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
Visibility="{x:Bind ViewModel.DragVisibility, Mode=OneWay}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Text="Drage Project Folder Here" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Empty Place Holder -->
|
||||
<Grid
|
||||
x:Name="EmptyPlaceHolder"
|
||||
Grid.Row="2"
|
||||
Visibility="{x:Bind ViewModel.EmptyVisibility, Mode=OneWay}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Text="No projects found" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -1,72 +0,0 @@
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Editor.ViewModels.Pages.Landing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Ghost.Editor.View.Pages.Landing;
|
||||
|
||||
internal sealed partial class OpenProjectPage : Page
|
||||
{
|
||||
public OpenProjectViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public OpenProjectPage()
|
||||
{
|
||||
ViewModel = App.GetService<OpenProjectViewModel>();
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
ViewModel.OnNavigatedTo(e.Parameter);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedFrom(e);
|
||||
ViewModel.OnNavigatedFrom();
|
||||
}
|
||||
|
||||
private void ProjectContainer_DragEnter(object sender, DragEventArgs e)
|
||||
{
|
||||
ViewModel.DragVisibility = Visibility.Visible;
|
||||
ViewModel.EmptyVisibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ProjectContainer_DragLeave(object sender, DragEventArgs e)
|
||||
{
|
||||
ViewModel.DragVisibility = Visibility.Collapsed;
|
||||
ViewModel.UpdateEmptyPlaceHolderVisibility();
|
||||
}
|
||||
|
||||
private void ProjectContainer_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
if (e.DataView.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
e.AcceptedOperation = DataPackageOperation.Link;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.AcceptedOperation = DataPackageOperation.None;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ProjectContainer_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
await ViewModel.ContentDrop(e.DataView);
|
||||
}
|
||||
|
||||
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is ProjectMetadataInfo project)
|
||||
{
|
||||
await Task.Yield();
|
||||
await ViewModel.OpenProjectAsync(project);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winex:WindowEx
|
||||
x:Class="Ghost.Editor.View.Windows.EngineEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ee="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:internal="using:Ghost.Editor.Controls.Internal"
|
||||
xmlns:local="using:Ghost.Editor.View.Windows"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winex="using:WinUIEx"
|
||||
Activated="WindowEx_Activated"
|
||||
Closed="WindowEx_Closed"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Titlebar -->
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Padding="8"
|
||||
Orientation="Horizontal">
|
||||
<ImageIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center"
|
||||
Source="ms-appx:///Assets/Icon.targetsize-32.png" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.engineVersionDescriptor}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.CurrentProject.Metadata.Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Grid Grid.Row="1" Margin="4,4">
|
||||
<controls:TabbedCommandBar>
|
||||
<controls:TabbedCommandBar.MenuItems>
|
||||
<controls:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</controls:TabbedCommandBarItem>
|
||||
<controls:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</controls:TabbedCommandBarItem>
|
||||
<controls:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</controls:TabbedCommandBarItem>
|
||||
</controls:TabbedCommandBar.MenuItems>
|
||||
</controls:TabbedCommandBar>
|
||||
</Grid>
|
||||
|
||||
<!-- Editor -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<internal:NavigationTabView
|
||||
Grid.Column="0"
|
||||
Width="350"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<internal:NavigationTabView.TabItems>
|
||||
<ee:HierarchyPage Header="Hierarchy">
|
||||
<ee:HierarchyPage.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</ee:HierarchyPage.IconSource>
|
||||
</ee:HierarchyPage>
|
||||
</internal:NavigationTabView.TabItems>
|
||||
</internal:NavigationTabView>
|
||||
|
||||
<internal:NavigationTabView Grid.Column="1">
|
||||
<internal:NavigationTabView.TabItems>
|
||||
<ee:ScenePage Header="Scene">
|
||||
<ee:ScenePage.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</ee:ScenePage.IconSource>
|
||||
</ee:ScenePage>
|
||||
</internal:NavigationTabView.TabItems>
|
||||
</internal:NavigationTabView>
|
||||
|
||||
<internal:NavigationTabView
|
||||
Grid.Column="2"
|
||||
Width="350"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<internal:NavigationTabView.TabItems>
|
||||
<ee:InspectorPage Header="Inspector">
|
||||
<ee:InspectorPage.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</ee:InspectorPage.IconSource>
|
||||
</ee:InspectorPage>
|
||||
</internal:NavigationTabView.TabItems>
|
||||
</internal:NavigationTabView>
|
||||
</Grid>
|
||||
|
||||
<internal:NavigationTabView Grid.Row="1" Height="350">
|
||||
<internal:NavigationTabView.TabItems>
|
||||
<TabViewItem Header="Project">
|
||||
<TabViewItem.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</TabViewItem.IconSource>
|
||||
<ee:ProjectPage />
|
||||
</TabViewItem>
|
||||
<TabViewItem Header="Console">
|
||||
<TabViewItem.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</TabViewItem.IconSource>
|
||||
<ee:ConsolePage />
|
||||
</TabViewItem>
|
||||
</internal:NavigationTabView.TabItems>
|
||||
</internal:NavigationTabView>
|
||||
</Grid>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
Height="25"
|
||||
Background="{ThemeResource SolidBackgroundFillColorBaseAltBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||
Glyph=""
|
||||
Visibility="Visible" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Visibility="Collapsed">
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorAttentionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="0" Grid.RowSpan="4">
|
||||
<InfoBar
|
||||
x:Name="InfoBar"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom">
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</InfoBar>
|
||||
|
||||
<Grid
|
||||
x:Name="ProgressBarContainer"
|
||||
Background="{ThemeResource SmokeFillColorDefaultBrush}"
|
||||
Visibility="Collapsed">
|
||||
<Grid
|
||||
Height="100"
|
||||
Padding="36,24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="{ThemeResource SolidBackgroundFillColorBaseBrush}"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
x:Name="ProgressMessage"
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,12"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Text="Loading..." />
|
||||
<ProgressBar
|
||||
x:Name="ProgressBar"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</winex:WindowEx>
|
||||
@@ -1,54 +0,0 @@
|
||||
using Ghost.Data.Resources;
|
||||
using Ghost.Editor.Core.Notifications;
|
||||
using Ghost.Editor.Core.Progress;
|
||||
using Ghost.Editor.ViewModels.Windows;
|
||||
using Ghost.Engine.Resources;
|
||||
using WinUIEx;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Ghost.Editor.View.Windows;
|
||||
/// <summary>
|
||||
/// An empty window that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
internal sealed partial class EngineEditorWindow : WindowEx
|
||||
{
|
||||
private readonly NotificationService _notificationService;
|
||||
private readonly ProgressService _progressService;
|
||||
|
||||
public EngineEditorViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public EngineEditorWindow()
|
||||
{
|
||||
ViewModel = App.GetService<EngineEditorViewModel>();
|
||||
|
||||
_notificationService = (NotificationService)App.GetService<INotificationService>();
|
||||
_progressService = (ProgressService)App.GetService<IProgressService>();
|
||||
|
||||
AppWindow.SetIcon(AssetsPath.s_appIconPath);
|
||||
Title = EngineData.ENGINE_NAME;
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
this.CenterOnScreen();
|
||||
}
|
||||
|
||||
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
|
||||
{
|
||||
Bindings.Update();
|
||||
|
||||
_notificationService.SetReference(InfoBar, NotificationQueue);
|
||||
_progressService.SetReference(ProgressBarContainer);
|
||||
}
|
||||
|
||||
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
{
|
||||
_notificationService.ClearReference();
|
||||
_progressService.ClearReference();
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winex:WindowEx
|
||||
x:Class="Ghost.Editor.View.Windows.LandingWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:Ghost.Editor.View.Windows"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:winex="using:WinUIEx"
|
||||
Activated="WindowEx_Activated"
|
||||
Closed="WindowEx_Closed"
|
||||
IsResizable="False"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0">
|
||||
<TextBlock
|
||||
Margin="24,0,0,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="Ghost Engine" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Padding="24,0,24,18">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<SelectorBar
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Right"
|
||||
SelectionChanged="SelectorBar_SelectionChanged">
|
||||
<SelectorBarItem IsSelected="True" Text="Open">
|
||||
<SelectorBarItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</SelectorBarItem.Icon>
|
||||
</SelectorBarItem>
|
||||
<SelectorBarItem Text="Create">
|
||||
<SelectorBarItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</SelectorBarItem.Icon>
|
||||
</SelectorBarItem>
|
||||
</SelectorBar>
|
||||
|
||||
<Frame
|
||||
x:Name="ContentFrame"
|
||||
Grid.Row="1"
|
||||
Padding="8"
|
||||
CacheMode="BitmapCache"
|
||||
CacheSize="10" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Padding="16">
|
||||
<InfoBar
|
||||
x:Name="InfoBar"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom">
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</InfoBar>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</winex:WindowEx>
|
||||
@@ -1,59 +0,0 @@
|
||||
using Ghost.Data.Resources;
|
||||
using Ghost.Editor.Core.Notifications;
|
||||
using Ghost.Editor.View.Pages.Landing;
|
||||
using Ghost.Engine.Resources;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Ghost.Editor.View.Windows;
|
||||
|
||||
internal sealed partial class LandingWindow : WindowEx
|
||||
{
|
||||
private readonly NotificationService _notificationService;
|
||||
|
||||
private int _previousSelectedIndex;
|
||||
|
||||
public LandingWindow()
|
||||
{
|
||||
_notificationService = (NotificationService)App.GetService<INotificationService>();
|
||||
|
||||
AppWindow.SetIcon(AssetsPath.s_appIconPath);
|
||||
Title = EngineData.ENGINE_NAME;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
this.SetWindowSize(1000, 750);
|
||||
this.CenterOnScreen();
|
||||
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
}
|
||||
|
||||
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
|
||||
{
|
||||
_notificationService.SetReference(InfoBar, NotificationQueue);
|
||||
}
|
||||
|
||||
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
{
|
||||
_notificationService.ClearReference();
|
||||
}
|
||||
|
||||
private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e)
|
||||
{
|
||||
var selectedItem = sender.SelectedItem;
|
||||
var currentSelectedIndex = sender.Items.IndexOf(selectedItem);
|
||||
var pageType = currentSelectedIndex switch
|
||||
{
|
||||
1 => typeof(CreateProjectPage),
|
||||
_ => typeof(OpenProjectPage),
|
||||
};
|
||||
|
||||
var slideNavigationTransitionEffect = currentSelectedIndex - _previousSelectedIndex > 0 ?
|
||||
SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft;
|
||||
|
||||
ContentFrame.Navigate(pageType, null, new SlideNavigationTransitionInfo() { Effect = slideNavigationTransitionEffect });
|
||||
|
||||
_previousSelectedIndex = currentSelectedIndex;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Ghost.Core;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class ConsoleViewModel : ObservableObject
|
||||
{
|
||||
public ReadOnlyObservableCollection<LogMessage> Logs => Logger.Logs;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowInfo
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowWarning
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowError
|
||||
{
|
||||
get; set;
|
||||
} = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowStackTrace
|
||||
{
|
||||
get; set;
|
||||
} = false;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial LogMessage? SelectedLog
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
partial void OnShowStackTraceChanged(bool value)
|
||||
{
|
||||
//Logger.HasStackTrace = value;
|
||||
//Logger.LogInfo($"Stack trace visibility set to {value}.");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearLogs()
|
||||
{
|
||||
Logger.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.SceneGraph;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class HierarchyViewModel : ObservableObject, INavigationAware
|
||||
{
|
||||
//[ObservableProperty]
|
||||
//public partial ObservableCollection<SceneNode> SceneList
|
||||
//{
|
||||
// get;
|
||||
// private set;
|
||||
//} = new(EditorSceneManager.LoadedWorlds);
|
||||
|
||||
//private void OnWorldLoaded(SceneNode node)
|
||||
//{
|
||||
// SceneList.Add(node);
|
||||
//}
|
||||
|
||||
//private void OnWorldUnloaded(SceneNode node)
|
||||
//{
|
||||
// SceneList.Remove(node);
|
||||
//}
|
||||
|
||||
public void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
//EditorSceneManager.OnWorldLoaded += OnWorldLoaded;
|
||||
//EditorSceneManager.OnWorldUnloaded += OnWorldUnloaded;
|
||||
}
|
||||
|
||||
public void OnNavigatedFrom()
|
||||
{
|
||||
//EditorSceneManager.OnWorldLoaded -= OnWorldLoaded;
|
||||
//EditorSceneManager.OnWorldUnloaded -= OnWorldUnloaded;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.Inspector;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class InspectorViewModel(IInspectorService inspectorService) : ObservableObject, INavigationAware
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial IInspectable? Inspectable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
inspectorService.OnSelectionChanged += OnSelectionChanged;
|
||||
Inspectable = inspectorService.SelectedInspectable;
|
||||
}
|
||||
|
||||
public void OnNavigatedFrom()
|
||||
{
|
||||
inspectorService.OnSelectionChanged -= OnSelectionChanged;
|
||||
Inspectable = null;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged()
|
||||
{
|
||||
Inspectable = inspectorService.SelectedInspectable;
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.Core.AssetHandle;
|
||||
using Ghost.Editor.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.EngineEditor;
|
||||
|
||||
internal partial class ProjectViewModel : ObservableObject
|
||||
{
|
||||
public ObservableCollection<ExplorerItem> SubDirectories
|
||||
{
|
||||
get;
|
||||
} = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ExplorerItem> DirectoryAssets
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ExplorerItem? SelectedDirectory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ExplorerItem? SelectedAsset
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public ProjectViewModel()
|
||||
{
|
||||
if (ProjectService.CurrentProject.Metadata == null)
|
||||
{
|
||||
throw new InvalidOperationException("Current project is not set.");
|
||||
}
|
||||
|
||||
var assetsRootItem = new ExplorerItem("Assets", Path.Combine(Path.GetDirectoryName(ProjectService.CurrentProject.Path)!, ProjectService.ASSETS_FOLDER), true);
|
||||
LoadSubFolderRecursive(ref assetsRootItem);
|
||||
|
||||
SubDirectories.Add(assetsRootItem);
|
||||
}
|
||||
|
||||
private static void LoadSubFolderRecursive(ref ExplorerItem parentItem)
|
||||
{
|
||||
foreach (var directory in Directory.EnumerateDirectories(parentItem.FullName))
|
||||
{
|
||||
var item = new ExplorerItem(Path.GetFileName(directory), directory, true);
|
||||
LoadSubFolderRecursive(ref item);
|
||||
|
||||
parentItem.Children ??= new();
|
||||
parentItem.Children.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task<ExplorerItem?> FindNodeIterative(ExplorerItem root, Func<ExplorerItem, bool> predicate)
|
||||
{
|
||||
var stack = new Stack<ExplorerItem>();
|
||||
stack.Push(root);
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var node = stack.Pop();
|
||||
if (predicate(node))
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.Children == null || node.Children.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
stack.Push(node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void NavigateToDirectory(string? path)
|
||||
{
|
||||
App.Window?.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
DirectoryAssets.Clear();
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.EnumerateDirectories(path))
|
||||
{
|
||||
var directoryItem = new ExplorerItem(Path.GetFileName(directory), directory, true);
|
||||
DirectoryAssets.Add(directoryItem);
|
||||
}
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(path))
|
||||
{
|
||||
var fileItem = new ExplorerItem(Path.GetFileName(file), file, false);
|
||||
DirectoryAssets.Add(fileItem);
|
||||
}
|
||||
|
||||
SelectedDirectory = await FindNodeIterative(SubDirectories[0], x => x.FullName == path);
|
||||
});
|
||||
}
|
||||
|
||||
public void OpenSelected()
|
||||
{
|
||||
if (SelectedAsset == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedAsset.IsDirectory)
|
||||
{
|
||||
NavigateToDirectory(SelectedAsset.FullName);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.OpenAsset(SelectedAsset.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedDirectoryChanged(ExplorerItem? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DirectoryAssets.Clear();
|
||||
NavigateToDirectory(value.FullName);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.Core.AppState;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.Notifications;
|
||||
using Ghost.Editor.Utilities;
|
||||
using Ghost.Engine.Resources;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.Landing;
|
||||
|
||||
internal partial class CreateProjectViewModel(INotificationService notificationService, ProjectService projectService, AppStateMachine stateService) : ObservableObject, INavigationAware
|
||||
{
|
||||
public ObservableCollection<TemplateData> templates = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial TemplateData? SelectedTemplate
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? ProjectName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? ProjectLocation
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public async void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
templates.Clear();
|
||||
await foreach (var (path, info) in ProjectService.GetProjectTemplatesAsync())
|
||||
{
|
||||
templates.Add(new(path, info));
|
||||
}
|
||||
|
||||
SelectedTemplate = templates.FirstOrDefault();
|
||||
}
|
||||
|
||||
public void OnNavigatedFrom()
|
||||
{
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SelectionProjectLocation()
|
||||
{
|
||||
var folder = await SystemUtilities.OpenFolderPickerAsync();
|
||||
if (folder != null)
|
||||
{
|
||||
ProjectLocation = folder.Path;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreateProject()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ProjectName)
|
||||
|| !Directory.Exists(ProjectLocation)
|
||||
|| !SelectedTemplate.HasValue)
|
||||
{
|
||||
notificationService.ShowNotification("Incorrect project info", MessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, EngineData.EngineVersion, SelectedTemplate.Value.directory);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
notificationService.ShowNotification(result.Message, MessageType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await stateService.TransitionToAsync(StateKey.EngineEditor, result.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
notificationService.ShowNotification($"Failed to load project: {e.Message}", MessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Editor.Core.AppState;
|
||||
using Ghost.Editor.Core.Contracts;
|
||||
using Ghost.Editor.Core.Notifications;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Collections.ObjectModel;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Pages.Landing;
|
||||
|
||||
internal partial class OpenProjectViewModel(ProjectService projectService, INotificationService _notificationService, AppStateMachine _stateService) : ObservableObject, INavigationAware
|
||||
{
|
||||
public readonly ObservableCollection<ProjectMetadataInfo> projects = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Visibility EmptyVisibility
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Visibility DragVisibility
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void UpdateEmptyPlaceHolderVisibility()
|
||||
{
|
||||
EmptyVisibility = projects.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public async void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
await foreach (var projectInfo in projectService.GetAllProjectAsync())
|
||||
{
|
||||
var metadata = await ProjectService.LoadMetadataAsync(projectInfo.MetadataPath);
|
||||
if (metadata == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
projects.Add(new(projectInfo.MetadataPath, metadata));
|
||||
}
|
||||
|
||||
UpdateEmptyPlaceHolderVisibility();
|
||||
DragVisibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public void OnNavigatedFrom()
|
||||
{
|
||||
projects.Clear();
|
||||
}
|
||||
|
||||
public async Task ContentDrop(DataPackageView dataView)
|
||||
{
|
||||
var errorMessage = string.Empty;
|
||||
if (dataView.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var items = await dataView.GetStorageItemsAsync();
|
||||
var rootFolder = items.OfType<StorageFolder>().FirstOrDefault();
|
||||
if (rootFolder != null)
|
||||
{
|
||||
var result = await projectService.AddProjectFromDirectoryAsync(rootFolder.Path);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
projects.Add(result.Value);
|
||||
goto CloseDropPanel;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = result.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Unsupported data format. Please drop a folder containing a project.";
|
||||
}
|
||||
|
||||
_notificationService.ShowNotification(errorMessage, MessageType.Error);
|
||||
|
||||
CloseDropPanel:
|
||||
DragVisibility = Visibility.Collapsed;
|
||||
UpdateEmptyPlaceHolderVisibility();
|
||||
}
|
||||
|
||||
public async Task OpenProjectAsync(ProjectMetadataInfo project)
|
||||
{
|
||||
try
|
||||
{
|
||||
project.Metadata.LastOpened = DateTime.Now;
|
||||
await ProjectService.CreateMetadataFileAsync(project.Path, project.Metadata);
|
||||
|
||||
await _stateService.TransitionToAsync(StateKey.EngineEditor, project);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_notificationService.ShowNotification($"Failed to load project: {e.Message}", MessageType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Ghost.Data.Models;
|
||||
using Ghost.Data.Services;
|
||||
using Ghost.Engine.Resources;
|
||||
|
||||
namespace Ghost.Editor.ViewModels.Windows;
|
||||
|
||||
internal partial class EngineEditorViewModel : ObservableRecipient
|
||||
{
|
||||
public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.EngineVersion}";
|
||||
|
||||
public ProjectMetadataInfo CurrentProject => ProjectService.CurrentProject;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using Ghost.Entities;
|
||||
using Misaki.HighPerformance.Jobs;
|
||||
|
||||
namespace Ghost.Engine;
|
||||
|
||||
public interface IEngineContext
|
||||
{
|
||||
JobScheduler JobScheduler { get; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
internal class EngineEntryAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
[EngineEntry]
|
||||
internal sealed partial class EngineCore : IEngineContext
|
||||
{
|
||||
private readonly JobScheduler _jobScheduler;
|
||||
|
||||
public JobScheduler JobScheduler => _jobScheduler;
|
||||
|
||||
public EngineCore()
|
||||
{
|
||||
_jobScheduler = new JobScheduler(Environment.ProcessorCount - 2); // We -2 here, one for main thread, one for render thread
|
||||
|
||||
ComponentRegistry.GetOrRegisterComponentID<ManagedEntityRef>();
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_jobScheduler.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
|
||||
<ProjectReference Include="..\Ghost.Test.Core\Ghost.Test.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,7 +0,0 @@
|
||||
using Ghost.Entities.Test;
|
||||
using Ghost.Test.Core;
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
AllocationManager.EnableDebugLayer();
|
||||
TestRunner.Run<SerializationTest>();
|
||||
AllocationManager.Dispose();
|
||||
@@ -1,176 +0,0 @@
|
||||
#if false
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
namespace Ghost.Entities;
|
||||
|
||||
public interface ISharedComponent
|
||||
{
|
||||
}
|
||||
|
||||
internal unsafe sealed class SharedComponentStore : IDisposable
|
||||
{
|
||||
private struct EntryInfo
|
||||
{
|
||||
public int RefCount;
|
||||
public int HashCode;
|
||||
public int Version;
|
||||
public int NextFree; // free-list linkage (index)
|
||||
}
|
||||
|
||||
private struct TypeStore : IDisposable
|
||||
{
|
||||
public int TypeSize;
|
||||
public UnsafeList<byte> Data; // raw bytes, stride = TypeSize
|
||||
public UnsafeList<EntryInfo> Infos; // parallel to Data entries (Entry 0 reserved)
|
||||
public UnsafeHashMap<long, int> HashLookup; // (hashKey) -> entryIndex
|
||||
public int FreeListHead; // head index, 0 means none (we'll use Infos[0].NextFree too if you prefer)
|
||||
public int VersionCounter;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Data.Dispose();
|
||||
Infos.Dispose();
|
||||
HashLookup.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly UnsafeHashMap<int, TypeStore> _perType; // componentTypeId -> TypeStore
|
||||
|
||||
public SharedComponentStore(int initialCapacity = 16)
|
||||
{
|
||||
_perType = new UnsafeHashMap<int, TypeStore>(initialCapacity, Allocator.Persistent);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var kvp in _perType)
|
||||
{
|
||||
kvp.Value.Dispose();
|
||||
}
|
||||
|
||||
_perType.Dispose();
|
||||
}
|
||||
|
||||
public int InsertOrGet(int componentTypeId, int typeSize, void* data, int hashCode)
|
||||
{
|
||||
// Reserve index 0 for "default"
|
||||
if (data == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
ref var store = ref GetOrCreateTypeStore(componentTypeId, typeSize);
|
||||
|
||||
// Combine (typeId, hash) into a single key; collisions handled by memcmp below.
|
||||
var key = ((long)componentTypeId << 32) ^ (uint)hashCode;
|
||||
|
||||
if (store.HashLookup.TryGetValue(key, out var existingIndex))
|
||||
{
|
||||
var existingPtr = (byte*)store.Data.GetUnsafePtr() + (existingIndex * store.TypeSize);
|
||||
if (new Span<byte>(existingPtr, store.TypeSize).SequenceEqual(new Span<byte>(data, store.TypeSize)))
|
||||
{
|
||||
((EntryInfo*)store.Infos.GetUnsafePtr())[existingIndex].RefCount++;
|
||||
return existingIndex;
|
||||
}
|
||||
// If collision: fall through to insert (you may want a secondary structure).
|
||||
}
|
||||
|
||||
int index = AllocateEntry(ref store);
|
||||
|
||||
var dst = (byte*)store.Data.GetUnsafePtr() + (index * store.TypeSize);
|
||||
MemoryUtility.MemCpy(dst, data, (nuint)store.TypeSize);
|
||||
|
||||
store.Infos[index] = new EntryInfo
|
||||
{
|
||||
RefCount = 1,
|
||||
HashCode = hashCode,
|
||||
Version = ++store.VersionCounter,
|
||||
NextFree = -1
|
||||
};
|
||||
|
||||
store.HashLookup[key] = index;
|
||||
return index;
|
||||
}
|
||||
|
||||
public void AddRef(int componentTypeId, int index)
|
||||
{
|
||||
if (index == 0) return;
|
||||
ref var store = ref _perType[componentTypeId];
|
||||
store.Infos[index].RefCount++;
|
||||
}
|
||||
|
||||
public void Release(int componentTypeId, int index)
|
||||
{
|
||||
if (index == 0) return;
|
||||
ref var store = ref _perType.GetValueByKey(componentTypeId);
|
||||
|
||||
ref var info = ref store.Infos.Ptr[index];
|
||||
info.RefCount--;
|
||||
if (info.RefCount > 0) return;
|
||||
|
||||
// Remove from hash lookup (best-effort; collisions require more robust handling)
|
||||
long key = ((long)componentTypeId << 32) ^ (uint)info.HashCode;
|
||||
store.HashLookup.Remove(key);
|
||||
|
||||
// Push to free-list
|
||||
info.NextFree = store.FreeListHead;
|
||||
store.FreeListHead = index;
|
||||
}
|
||||
|
||||
public void* GetDataPtr(int componentTypeId, int index)
|
||||
{
|
||||
if (index == 0) return null;
|
||||
ref var store = ref _perType.GetValueByKey(componentTypeId);
|
||||
return (byte*)store.Data.Ptr + (index * store.TypeSize);
|
||||
}
|
||||
|
||||
private ref TypeStore GetOrCreateTypeStore(int componentTypeId, int typeSize)
|
||||
{
|
||||
if (_perType.TryGetValue(componentTypeId, out var existing))
|
||||
{
|
||||
// UnsafeHashMap returns by value in some implementations; you may need a different pattern here.
|
||||
// Adjust to your container API (e.g., TryGetValueRef).
|
||||
}
|
||||
|
||||
var store = new TypeStore
|
||||
{
|
||||
TypeSize = typeSize,
|
||||
Data = new UnsafeList<byte>(typeSize * 16, Allocator.Persistent),
|
||||
Infos = new UnsafeList<EntryInfo>(16, Allocator.Persistent),
|
||||
HashLookup = new UnsafeHashMap<long, int>(16, Allocator.Persistent),
|
||||
FreeListHead = 0,
|
||||
VersionCounter = 0
|
||||
};
|
||||
|
||||
// Create reserved default entry at index 0
|
||||
store.Data.Resize(typeSize); // one element worth of bytes
|
||||
store.Infos.Add(new EntryInfo { RefCount = int.MaxValue, HashCode = 0, Version = 0, NextFree = -1 });
|
||||
|
||||
_perType.Add(componentTypeId, store);
|
||||
// NOTE: returning a ref requires a "get ref" API; adjust to your UnsafeHashMap capabilities.
|
||||
return ref _perType.GetValueByKey(componentTypeId);
|
||||
}
|
||||
|
||||
private static int AllocateEntry(ref TypeStore store)
|
||||
{
|
||||
if (store.FreeListHead != 0)
|
||||
{
|
||||
int idx = store.FreeListHead;
|
||||
store.FreeListHead = store.Infos[idx].NextFree;
|
||||
store.Infos[idx].NextFree = -1;
|
||||
return idx;
|
||||
}
|
||||
|
||||
int newIndex = store.Infos.Count;
|
||||
store.Infos.Add(default);
|
||||
|
||||
int newByteCount = (newIndex + 1) * store.TypeSize;
|
||||
store.Data.Resize(newByteCount);
|
||||
|
||||
return newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,52 +0,0 @@
|
||||
namespace Ghost.Graphics.Test.Models;
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Debug
|
||||
}
|
||||
|
||||
internal struct LogItem
|
||||
{
|
||||
public LogLevel Level
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
public string Message
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
public DateTime Timestamp
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
public string? StackTrace
|
||||
{
|
||||
get; init;
|
||||
}
|
||||
|
||||
public LogItem(LogLevel level, string message, string? stackTrace = null)
|
||||
{
|
||||
Level = level;
|
||||
Message = message;
|
||||
StackTrace = stackTrace;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"{Timestamp:HH:mm:ss.fff} [{Level}] {Message}";
|
||||
}
|
||||
|
||||
public readonly string ToStringWithStackTrace()
|
||||
{
|
||||
if (string.IsNullOrEmpty(StackTrace))
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
|
||||
return $"{ToString()}\n{StackTrace}";
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
using Ghost.Graphics.Test.Models;
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ghost.Graphics.Test.Services;
|
||||
|
||||
internal class LoggingService
|
||||
{
|
||||
private const int MAX_LOGS = 4096;
|
||||
private static readonly Lazy<LoggingService> _instance = new(() => new LoggingService());
|
||||
|
||||
private readonly List<LogItem> _logs = [];
|
||||
private readonly object _lockObject = new();
|
||||
|
||||
public static LoggingService Instance => _instance.Value;
|
||||
|
||||
public IReadOnlyList<LogItem> Logs
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _logs.AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CaptureStackTrace { get; set; } = false;
|
||||
|
||||
public event Action<LogItem>? LogAdded;
|
||||
public event Action? LogsCleared;
|
||||
|
||||
private LoggingService()
|
||||
{
|
||||
}
|
||||
|
||||
private void AddLog(LogItem logItem)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_logs.Count >= MAX_LOGS)
|
||||
{
|
||||
_logs.RemoveAt(0);
|
||||
}
|
||||
|
||||
_logs.Add(logItem);
|
||||
}
|
||||
|
||||
// Invoke event outside of lock to prevent deadlock
|
||||
LogAdded?.Invoke(logItem);
|
||||
}
|
||||
|
||||
private string? CaptureCurrentStackTrace()
|
||||
{
|
||||
if (!CaptureStackTrace)
|
||||
return null;
|
||||
|
||||
var stackTrace = new StackTrace(skipFrames: 2, fNeedFileInfo: true);
|
||||
return stackTrace.ToString();
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, object? message)
|
||||
{
|
||||
var stackTrace = CaptureCurrentStackTrace();
|
||||
var logItem = new LogItem(level, message?.ToString() ?? string.Empty, stackTrace);
|
||||
AddLog(logItem);
|
||||
}
|
||||
|
||||
public void LogInfo(object? message)
|
||||
{
|
||||
Log(LogLevel.Info, message);
|
||||
}
|
||||
|
||||
public void LogWarning(object? message)
|
||||
{
|
||||
Log(LogLevel.Warning, message);
|
||||
}
|
||||
|
||||
public void LogError(object? message)
|
||||
{
|
||||
Log(LogLevel.Error, message);
|
||||
}
|
||||
|
||||
public void LogError(Exception exception)
|
||||
{
|
||||
var logItem = new LogItem(LogLevel.Error, exception.Message, exception.StackTrace);
|
||||
AddLog(logItem);
|
||||
}
|
||||
|
||||
public void LogDebug(object? message)
|
||||
{
|
||||
Log(LogLevel.Debug, message);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_logs.Clear();
|
||||
}
|
||||
|
||||
LogsCleared?.Invoke();
|
||||
}
|
||||
|
||||
// Static methods for easier usage throughout the test project
|
||||
public static void Info(object? message) => Instance.LogInfo(message);
|
||||
public static void Warning(object? message) => Instance.LogWarning(message);
|
||||
public static void Error(object? message) => Instance.LogError(message);
|
||||
public static void Error(Exception exception) => Instance.LogError(exception);
|
||||
public static void Debug(object? message) => Instance.LogDebug(message);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Misaki.HighPerformance.Mathematics;
|
||||
using static Ghost.Graphics.D3D12.D3D12ResourceDatabase;
|
||||
|
||||
namespace Ghost.Graphics.Test.Windows;
|
||||
|
||||
public sealed partial class GraphicsTestWindow : Window
|
||||
{
|
||||
private IRenderSystem? _renderSystem;
|
||||
private IRenderer? _renderer;
|
||||
private ISwapChain? _swapChain;
|
||||
|
||||
private bool _isFirstActivationHandled;
|
||||
|
||||
public unsafe GraphicsTestWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Activated += GraphicsTestWindow_Activated;
|
||||
Closed += GraphicsTestWindow_Closed;
|
||||
|
||||
Panel.SizeChanged += SwapChainPanel_SizeChanged;
|
||||
Panel.CompositionScaleChanged += SwapChainPanel_CompositionScaleChanged;
|
||||
}
|
||||
|
||||
private void GraphicsTestWindow_Activated(object sender, WindowActivatedEventArgs e)
|
||||
{
|
||||
if (_isFirstActivationHandled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.EnableDebugLayer();
|
||||
#endif
|
||||
|
||||
_renderSystem = new RenderSystem(new RenderingConfig()
|
||||
{
|
||||
FrameBufferCount = 2,
|
||||
GraphicsAPI = GraphicsAPI.Direct3D12
|
||||
});
|
||||
_renderer = _renderSystem.GraphicsEngine.CreateRenderer();
|
||||
_swapChain = _renderSystem.GraphicsEngine.CreateSwapChain(new SwapChainDesc
|
||||
{
|
||||
Width = (uint)AppWindow.Size.Width,
|
||||
Height = (uint)AppWindow.Size.Height,
|
||||
ScaleX = Panel.CompositionScaleX,
|
||||
ScaleY = Panel.CompositionScaleY,
|
||||
Format = TextureFormat.B8G8R8A8_UNorm,
|
||||
Target = SwapChainTarget.FromCompositionSurface(Panel)
|
||||
});
|
||||
|
||||
_renderer.RenderOutput = new SwapChainRenderOutput(_swapChain);
|
||||
|
||||
_renderSystem.Start();
|
||||
CompositionTarget.Rendering += OnRendering;
|
||||
|
||||
e.Handled = true;
|
||||
_isFirstActivationHandled = true;
|
||||
}
|
||||
|
||||
private void GraphicsTestWindow_Closed(object sender, WindowEventArgs e)
|
||||
{
|
||||
CompositionTarget.Rendering -= OnRendering;
|
||||
_renderSystem?.Stop();
|
||||
|
||||
_renderer?.Dispose();
|
||||
_swapChain?.Dispose();
|
||||
_renderSystem?.Dispose();
|
||||
|
||||
Misaki.HighPerformance.LowLevel.Buffer.AllocationManager.Dispose();
|
||||
}
|
||||
|
||||
private void SwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
if (_renderSystem == null || _swapChain == null || _renderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newWidth = (uint)(Panel.ActualWidth * Panel.CompositionScaleX);
|
||||
var newHeight = (uint)(Panel.ActualHeight * Panel.CompositionScaleY);
|
||||
|
||||
if (newWidth < 8 || newHeight < 8)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_renderSystem.RequestSwapChainResize(_swapChain, new uint2(newWidth, newHeight));
|
||||
_renderer.RenderOutput!.Viewport = new ViewportDesc { Width = newWidth, Height = newHeight, MinDepth = 0.0f, MaxDepth = 1.0f };
|
||||
_renderer.RenderOutput!.Scissor = new RectDesc { Right = newWidth, Bottom = newHeight };
|
||||
}
|
||||
|
||||
private void SwapChainPanel_CompositionScaleChanged(SwapChainPanel sender, object args)
|
||||
{
|
||||
_swapChain?.SetScale(sender.CompositionScaleX, sender.CompositionScaleY);
|
||||
}
|
||||
|
||||
private void OnRendering(object? sender, object e)
|
||||
{
|
||||
if (_renderSystem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_renderSystem.CPUFenceValue < _renderSystem.GPUFenceValue + _renderSystem.MaxFrameLatency)
|
||||
{
|
||||
_renderSystem.SignalCPUReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Ghost.Core;
|
||||
using Ghost.Graphics.Core;
|
||||
using Ghost.Graphics.RHI;
|
||||
|
||||
namespace Ghost.Graphics.Contracts;
|
||||
|
||||
public interface IRenderOutput
|
||||
{
|
||||
ViewportDesc Viewport
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
RectDesc Scissor
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a handle to the current render target texture.
|
||||
/// </summary>
|
||||
/// <returns>A handle to the texture that is currently set as the render target.</returns>
|
||||
Handle<Texture> GetRenderTarget();
|
||||
|
||||
/// <summary>
|
||||
/// Begins a rendering operation using the specified command buffer. Typically this will include resource barriers,
|
||||
/// </summary>
|
||||
/// <param name="cmd">The command buffer that records rendering commands.</param>
|
||||
///
|
||||
void BeginRender(ICommandBuffer cmd);
|
||||
/// <summary>
|
||||
/// Finalizes the rendering process using the specified command buffer.
|
||||
/// </summary>
|
||||
/// <param name="cmd">The command buffer that contains the rendering commands to be finalized.</param>
|
||||
void EndRender(ICommandBuffer cmd);
|
||||
|
||||
/// <summary>
|
||||
/// Displays the current frame to the output device or screen.
|
||||
/// </summary>
|
||||
/// <remarks>Call this method after rendering operations to present the rendered content. The exact
|
||||
/// behavior may depend on the underlying graphics implementation or device.</remarks>
|
||||
void Present();
|
||||
}
|
||||