Refactor folder structure

This commit is contained in:
2026-02-18 00:50:46 +09:00
parent 426786397c
commit db8ca971a8
413 changed files with 2885 additions and 3634 deletions

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Shader.Test")]
[assembly: InternalsVisibleTo("Ghost.Graphics")]

View File

@@ -0,0 +1,305 @@
using Misaki.HighPerformance.Mathematics;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Ghost.DSL.Generator;
public enum PackingRules
{
Exact,
Aligned,
}
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum)]
public class GenerateHLSLAttribute : Attribute
{
private readonly PackingRules _packingRules;
private readonly string? _outputSource;
public GenerateHLSLAttribute(PackingRules packingRules, string? outputSource)
{
_packingRules = packingRules;
_outputSource = outputSource;
}
}
internal static partial class ShaderStructGenerator
{
private struct ShaderFieldInfo
{
public string name;
public Type fieldType;
public ShaderFieldInfo(string name, Type fieldType)
{
this.name = name;
this.fieldType = fieldType;
}
public ShaderFieldInfo(FieldInfo fieldInfo)
: this(fieldInfo.Name, fieldInfo.FieldType)
{
}
}
private const int _HLSL_VECTOR_REGISTER_SIZE = 16; // 16 bytes (128 bits) for float4
private static void GenerateEnumHLSL(Type type, StringBuilder sb)
{
if (!type.IsEnum)
{
throw new InvalidOperationException($"Type {type.FullName} is not an enum.");
}
var enumName = type.Name;
//var underlyingType = Enum.GetUnderlyingType(space);
//var underlyingTypeName = underlyingType switch
//{
// Type t when t == typeof(byte) || t == typeof(short) || t == typeof(int) => "int",
// Type t when t == typeof(sbyte) || t == typeof(ushort) || t == typeof(uint) => "uint",
// _ => throw new InvalidOperationException($"Unsupported underlying space {underlyingType.FullName} for enum {enumName}."),
//};
// sb.Append(@$"
//enum {enumName} : {underlyingTypeName}
//{{");
var names = Enum.GetNames(type);
var values = Enum.GetValuesAsUnderlyingType(type);
for (var i = 0; i < names.Length; i++)
{
var name = $"{CamelCaseToUnderscoreRegex().Replace(enumName, "_$1")}_{names[i]}";
var value = values.GetValue(i);
// sb.Append(@$"
//{name} = {Value},");
sb.Append(@$"
#define {name.ToUpperInvariant()} {value}"); // Use #define for capability. Enum is only support for newer HLSL versions.
}
// sb.AppendLine(@"
//};");
sb.AppendLine();
}
public static int FindNextFieldThatFits(FieldInfo[] fields, bool[] looked, int startIndex, int size, out int foundIndex)
{
if (size <= 0)
{
foundIndex = -1;
return size;
}
var bestFitIndex = -1;
var bestFitSize = 0;
for (var j = startIndex; j < fields.Length; j++)
{
if (looked[j])
{
continue;
}
var nextField = fields[j];
var nextSize = Marshal.SizeOf(nextField.FieldType);
if (nextSize <= size)
{
if (nextSize == size)
{
foundIndex = j;
return nextSize;
}
if (nextSize > bestFitSize)
{
bestFitSize = nextSize;
bestFitIndex = j;
}
}
}
if (bestFitIndex != -1)
{
foundIndex = bestFitIndex;
return bestFitSize;
}
foundIndex = -1;
return size;
}
private static void GenerateStructHLSL(Type type, PackingRules packingRules, StringBuilder sb)
{
if (!type.IsValueType || type.IsPrimitive)
{
throw new InvalidOperationException($"Type {type.FullName} is not a struct.");
}
var structName = type.Name;
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance)
.Where(static f => f.FieldType.IsValueType).ToArray();
var shaderFields = new ShaderFieldInfo[fields.Length];
if (packingRules == PackingRules.Aligned)
{
var sortedFields = new List<ShaderFieldInfo>(fields.Length);
var looked = new bool[fields.Length];
var paddingIndex = 0;
// Sort the fields to align them to HLSL vector registers (16 bytes)
for (var i = 0; i < fields.Length; i++)
{
if (looked[i])
{
continue;
}
var field = fields[i];
var size = Marshal.SizeOf(field.FieldType);
sortedFields.Add(new ShaderFieldInfo(field));
var registerRemaining = _HLSL_VECTOR_REGISTER_SIZE - (size % _HLSL_VECTOR_REGISTER_SIZE);
while (true)
{
var nextSize = FindNextFieldThatFits(fields, looked, i + 1, registerRemaining, out var nextIndex);
if (nextSize == 0 || nextIndex == -1)
{
break;
}
looked[i] = true;
looked[nextIndex] = true;
sortedFields.Add(new ShaderFieldInfo(fields[nextIndex]));
registerRemaining -= nextSize;
}
if (registerRemaining != 0)
{
// Add padding if necessary
var count = registerRemaining / sizeof(float);
for (var p = 0; p < count; p++)
{
sortedFields.Add(new ShaderFieldInfo($"_padding{paddingIndex++}", typeof(float)));
}
}
}
shaderFields = sortedFields.ToArray();
}
else
{
for (var i = 0; i < fields.Length; i++)
{
shaderFields[i] = new ShaderFieldInfo(fields[i]);
}
}
sb.Append(@$"
struct {structName}
{{");
foreach (var field in shaderFields)
{
var fieldType = field.fieldType;
var fieldName = field.name;
string hlslType;
switch (fieldType)
{
case Type t when t == typeof(float):
hlslType = "float";
break;
case Type t when t == typeof(double):
hlslType = "double";
break;
case Type t when t == typeof(int):
hlslType = "int";
break;
case Type t when t == typeof(uint):
hlslType = "uint";
break;
case Type t when t == typeof(bool):
hlslType = "bool";
break;
case Type t when t == typeof(Vector2):
hlslType = "float2";
break;
case Type t when t == typeof(Vector3):
hlslType = "float3";
break;
case Type t when t == typeof(Vector4):
hlslType = "float4";
break;
case Type t when t == typeof(Matrix4x4):
hlslType = "float4x4";
break;
default:
{
if (fieldType.Namespace == typeof(float2).Namespace)
{
if (fieldType.Name.StartsWith("float")
|| fieldType.Name.StartsWith("double")
|| fieldType.Name.StartsWith("int")
|| fieldType.Name.StartsWith("uint")
|| fieldType.Name.StartsWith("bool"))
{
hlslType = fieldType.Name;
break;
}
}
throw new InvalidOperationException($"Unsupported field type: {fieldType.FullName} in struct {structName}.");
}
}
sb.Append(@$"
{hlslType} {fieldName};");
}
sb.AppendLine(@"
};");
}
public static void GenerateHLSL(ReadOnlySpan<Type> types, PackingRules packingRules, string outputSource)
{
if (!Directory.Exists(Path.GetDirectoryName(outputSource)))
{
throw new DirectoryNotFoundException($"The directory for the output source '{outputSource}' does not exist.");
}
var hlslDefine = $"{Path.GetFileNameWithoutExtension(outputSource).ToUpperInvariant().Replace('.', '_')}_HLSL";
var sb = new StringBuilder();
sb.AppendLine(@$"// Auto-generated HLSL code, please do not edit this file directly.
#ifndef {hlslDefine}
#define {hlslDefine}");
foreach (var type in types)
{
if (type.IsEnum)
{
GenerateEnumHLSL(type, sb);
}
else if (type.IsValueType && !type.IsPrimitive)
{
GenerateStructHLSL(type, packingRules, sb);
}
else
{
continue;
}
}
sb.Append(@"
#endif");
var hlslCode = sb.ToString();
File.WriteAllText(outputSource, hlslCode);
}
[GeneratedRegex("(?<=[a-z])([A-Z])")]
private static partial Regex CamelCaseToUnderscoreRegex();
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1" />
<PackageReference Include="Antlr4BuildTasks" Version="12.11.0" />
</ItemGroup>
<ItemGroup>
<Antlr4 Include="Grammar\GhostShaderLexer.g4">
<Generator>MSBuild:Compile</Generator>
<Listener>false</Listener>
<Visitor>true</Visitor>
</Antlr4>
<Antlr4 Include="Grammar\GhostShaderParser.g4">
<Generator>MSBuild:Compile</Generator>
<Listener>false</Listener>
<Visitor>true</Visitor>
</Antlr4>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Runtime/Ghost.Core/Ghost.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
lexer grammar GhostShaderLexer;
// Keywords
SHADER: 'shader';
PROPERTIES: 'properties';
PIPELINE: 'pipeline';
PASS: 'pass';
DEFINES: 'defines';
KEYWORDS: 'keywords';
INCLUDES: 'includes';
GLOBAL: 'global';
LOCAL: 'local';
HLSL: 'hlsl';
// Punctuation
LBRACE: '{';
RBRACE: '}';
LPAREN: '(';
RPAREN: ')';
LBRACK: '[';
RBRACK: ']';
SEMICOLON: ';';
COMMA: ',';
EQUALS: '=';
COLON: ':';
// Literals
STRING_LITERAL: '"' (~["\r\n] | '\\' .)* '"';
NUMBER: [0-9]+ ('.' [0-9]+)? | '.' [0-9]+;
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]*;
// Whitespace and Comments
WS: [ \t\r\n]+ -> skip;
LINE_COMMENT: '//' ~[\r\n]* -> skip;
BLOCK_COMMENT: '/*' .*? '*/' -> skip;
ANY_CHAR: . ;

View File

@@ -0,0 +1,99 @@
parser grammar GhostShaderParser;
options {
tokenVocab = GhostShaderLexer;
}
// Top-level rule
shaderFile: shader+ EOF;
shader:
SHADER STRING_LITERAL LBRACE
shaderBody
RBRACE;
shaderBody:
(propertiesBlock | pipelineBlock | passBlock | functionCall)*;
// Properties block
propertiesBlock:
PROPERTIES LBRACE
propertyDeclaration*
RBRACE;
propertyDeclaration:
scope? IDENTIFIER IDENTIFIER (EQUALS LBRACE propertyInitializer RBRACE)? SEMICOLON;
scope:
GLOBAL | LOCAL;
propertyInitializer:
(NUMBER | IDENTIFIER) (COMMA (NUMBER | IDENTIFIER))*;
// Pipeline block
pipelineBlock:
PIPELINE LBRACE
pipelineStatement*
RBRACE;
pipelineStatement:
IDENTIFIER EQUALS IDENTIFIER SEMICOLON;
// Pass block
passBlock:
PASS STRING_LITERAL LBRACE
passBody
RBRACE;
// Template
passBody:
(definesBlock | includesBlock | keywordsBlock | pipelineBlock | hlslBlock | shaderEntry)*;
definesBlock:
DEFINES LBRACE
defineStatement*
RBRACE;
defineStatement:
IDENTIFIER SEMICOLON;
includesBlock:
INCLUDES LBRACE
includeStatement*
RBRACE;
includeStatement:
STRING_LITERAL SEMICOLON;
keywordsBlock:
KEYWORDS LBRACE
keywordStatement*
RBRACE;
keywordStatement:
scope? IDENTIFIER (COMMA IDENTIFIER)* SEMICOLON;
hlslBlock:
HLSL LBRACE
hlslBody
RBRACE;
// Recursively matches content, ensuring braces are balanced.
hlslBody:
(
~(LBRACE | RBRACE) // Match ANY token except open/close braces
|
LBRACE hlslBody RBRACE // Or match a nested block recursively
)*;
shaderEntry:
IDENTIFIER STRING_LITERAL COLON STRING_LITERAL SEMICOLON;
functionCall:
IDENTIFIER LPAREN functionArguments? RPAREN SEMICOLON;
functionArguments:
functionArgument (COMMA functionArgument)*;
functionArgument:
STRING_LITERAL | NUMBER | IDENTIFIER;

View File

@@ -0,0 +1,327 @@
using Ghost.Core;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderParser;
using System.Runtime.CompilerServices;
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 int LayoutCBufferProperties(Span<PropertyDescriptor> properties)
{
if (properties.IsEmpty)
{
return 0;
}
var currentOffset = 0;
foreach (ref var prop in properties)
{
var size = prop.type.GetSize();
if ((currentOffset % 16) + size > 16)
{
currentOffset = (currentOffset + 15) & ~15;
}
prop.offset = currentOffset;
prop.size = size;
currentOffset += size;
}
return (currentOffset + 15) & ~15;
}
// 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 = LayoutCBufferProperties(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/src/Runtime//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/src/Runtime//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;
}
}

View File

@@ -0,0 +1,48 @@
using Ghost.Core.Graphics;
namespace Ghost.DSL.ShaderCompiler;
public enum PropertyScope
{
Global,
Local,
}
public class PropertySemantic
{
public PropertyScope scope;
public ShaderPropertyType type;
public string name = string.Empty;
public object? defaultValue;
}
public class PipelineSemantic
{
public ZTest? zTest;
public ZWrite? zWrite;
public Cull? cull;
public Blend? blend;
public ColorWriteMask? colorMask;
}
public class PassSemantic
{
public string name = string.Empty;
public ShaderEntryPoint taskShader;
public ShaderEntryPoint meshShader;
public ShaderEntryPoint pixelShader;
public string? hlsl;
public List<string>? defines;
public List<string>? includes;
public List<KeywordsGroup>? keywords;
public PipelineSemantic? localPipeline;
}
public class DSLShaderSemantics
{
public string name = string.Empty;
public string? hlsl;
public List<PropertySemantic>? properties;
public PipelineSemantic? pipeline;
public List<PassSemantic>? passes;
}

View File

@@ -0,0 +1,383 @@
using Antlr4.Runtime;
using Ghost.Core.Graphics;
using Ghost.DSL.ShaderCompiler;
using Ghost.DSL.ShaderParser.Model;
namespace Ghost.DSL.ShaderParser;
public class AntlrShaderCompiler
{
public static List<ShaderModel> ParseShaders(string source, out List<DSLShaderError> errors)
{
errors = new List<DSLShaderError>();
try
{
var inputStream = new AntlrInputStream(source);
var lexer = new GhostShaderLexer(inputStream);
// Capture lexer errors
lexer.RemoveErrorListeners();
var lexerErrorListener = new ErrorListener(errors);
lexer.AddErrorListener(lexerErrorListener);
var tokenStream = new CommonTokenStream(lexer);
var parser = new GhostShaderParser(tokenStream);
// Capture parser errors
parser.RemoveErrorListeners();
var parserErrorListener = new ErrorListener(errors);
parser.AddErrorListener(parserErrorListener);
var tree = parser.shaderFile();
if (errors.Count > 0)
{
return new List<ShaderModel>();
}
var visitor = new ShaderVisitor();
visitor.Visit(tree);
return visitor.Shaders;
}
catch (Exception ex)
{
errors.Add(new DSLShaderError
{
message = $"Unexpected error during parsing: {ex.Message}",
line = -1,
column = -1
});
return new List<ShaderModel>();
}
}
public static DSLShaderSemantics? ConvertToSemantics(ShaderModel model, out List<DSLShaderError> errors)
{
errors = new List<DSLShaderError>();
if (string.IsNullOrWhiteSpace(model.Name))
{
errors.Add(new DSLShaderError
{
message = "Shader name cannot be empty.",
line = 0,
column = 0
});
return null;
}
var semantics = new DSLShaderSemantics
{
name = model.Name,
properties = ConvertProperties(model.Properties, errors),
pipeline = ConvertPipeline(model.Pipeline, errors)
};
foreach (var pass in model.Passes)
{
var passSemantic = ConvertPass(pass, errors);
if (passSemantic != null)
{
semantics.passes ??= new List<PassSemantic>();
semantics.passes.Add(passSemantic);
}
}
return semantics;
}
private static List<PropertySemantic>? ConvertProperties(PropertiesBlockModel? properties, List<DSLShaderError> errors)
{
if (properties == null || properties.Properties.Count == 0)
{
return null;
}
var result = new List<PropertySemantic>();
var usedNames = new HashSet<string>();
foreach (var prop in properties.Properties)
{
if (usedNames.Contains(prop.Name))
{
errors.Add(new DSLShaderError
{
message = $"Duplicate property name '{prop.Name}'.",
line = 0,
column = 0
});
continue;
}
var semantic = new PropertySemantic
{
name = prop.Name,
scope = prop.Scope?.ToLower() == "global" ? PropertyScope.Global : PropertyScope.Local,
type = ParsePropertyType(prop.Type, errors)
};
if (prop.Initializer.Count > 0)
{
semantic.defaultValue = ParsePropertyValue(semantic.type, prop.Initializer, errors);
}
usedNames.Add(prop.Name);
result.Add(semantic);
}
return result;
}
private static ShaderPropertyType ParsePropertyType(string type, List<DSLShaderError> errors)
{
return type.ToLower() switch
{
"float" => ShaderPropertyType.Float,
"float2" => ShaderPropertyType.Float2,
"float3" => ShaderPropertyType.Float3,
"float4" => ShaderPropertyType.Float4,
"float4x4" => ShaderPropertyType.Float4x4,
"int" => ShaderPropertyType.Int,
"int2" => ShaderPropertyType.Int2,
"int3" => ShaderPropertyType.Int3,
"int4" => ShaderPropertyType.Int4,
"uint" => ShaderPropertyType.UInt,
"uint2" => ShaderPropertyType.UInt2,
"uint3" => ShaderPropertyType.UInt3,
"uint4" => ShaderPropertyType.UInt4,
"bool" => ShaderPropertyType.Bool,
"bool2" => ShaderPropertyType.Bool2,
"bool3" => ShaderPropertyType.Bool3,
"bool4" => ShaderPropertyType.Bool4,
"tex2d" => ShaderPropertyType.Texture2D,
"tex3d" => ShaderPropertyType.Texture3D,
"texcube" => ShaderPropertyType.TextureCube,
"texcube_arr" => ShaderPropertyType.TextureCubeArray,
"tex2d_arr" => ShaderPropertyType.Texture2DArray,
"sampler" => ShaderPropertyType.Sampler,
_ => ShaderPropertyType.None
};
}
private static object? ParsePropertyValue(ShaderPropertyType type, List<string> values, List<DSLShaderError> errors)
{
// For textures, the value is an identifier (e.g., "white", "black")
if (type is ShaderPropertyType.Texture2D or ShaderPropertyType.Texture3D or ShaderPropertyType.TextureCube)
{
return values.Count > 0 ? values[0] : null;
}
// For samplers, no default value
if (type == ShaderPropertyType.Sampler)
{
return null;
}
// For numeric types, parse the values
try
{
return type switch
{
ShaderPropertyType.Float => values.Count > 0 ? float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture) : 0f,
ShaderPropertyType.Float2 => values.Count >= 2 ? new Misaki.HighPerformance.Mathematics.float2(
float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Float3 => values.Count >= 3 ? new Misaki.HighPerformance.Mathematics.float3(
float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Float4 => values.Count >= 4 ? new Misaki.HighPerformance.Mathematics.float4(
float.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture),
float.Parse(values[3], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Int => values.Count > 0 ? int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture) : 0,
ShaderPropertyType.Int2 => values.Count >= 2 ? new Misaki.HighPerformance.Mathematics.int2(
int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Int3 => values.Count >= 3 ? new Misaki.HighPerformance.Mathematics.int3(
int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.Int4 => values.Count >= 4 ? new Misaki.HighPerformance.Mathematics.int4(
int.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[1], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[2], System.Globalization.CultureInfo.InvariantCulture),
int.Parse(values[3], System.Globalization.CultureInfo.InvariantCulture)) : default,
ShaderPropertyType.UInt => values.Count > 0 ? uint.Parse(values[0], System.Globalization.CultureInfo.InvariantCulture) : 0u,
ShaderPropertyType.Bool => values.Count > 0 && (values[0] == "1" || values[0].ToLower() == "true"),
_ => null
};
}
catch (Exception ex)
{
errors.Add(new DSLShaderError
{
message = $"Failed to parse property value: {ex.Message}",
line = 0,
column = 0
});
return null;
}
}
private static PipelineSemantic? ConvertPipeline(PipelineBlockModel? pipeline, List<DSLShaderError> errors)
{
if (pipeline == null || pipeline.Statements.Count == 0)
{
return null;
}
var semantic = new PipelineSemantic();
foreach (var (key, value) in pipeline.Statements)
{
switch (key.ToLower())
{
case "ztest":
semantic.zTest = value.ToLower() switch
{
"disabled" => ZTest.Disabled,
"less" => ZTest.Less,
"lessequal" => ZTest.LessEqual,
"equal" => ZTest.Equal,
"greaterequal" => ZTest.GreaterEqual,
"greater" => ZTest.Greater,
"notequal" => ZTest.NotEqual,
"always" => ZTest.Always,
_ => ZTest.Disabled
};
break;
case "zwrite":
semantic.zWrite = value.ToLower() == "on" ? ZWrite.On : ZWrite.Off;
break;
case "cull":
semantic.cull = value.ToLower() switch
{
"off" => Cull.Off,
"front" => Cull.Front,
"back" => Cull.Back,
_ => Cull.Off
};
break;
case "blend":
semantic.blend = value.ToLower() switch
{
"opaque" => Blend.Opaque,
"alpha" => Blend.Alpha,
"additive" => Blend.Additive,
"multiply" => Blend.Multiply,
"premultipliedalpha" => Blend.PremultipliedAlpha,
_ => Blend.Opaque
};
break;
case "color_mask":
semantic.colorMask = value.ToLower() == "all" ? ColorWriteMask.All : ColorWriteMask.None;
break;
}
}
return semantic;
}
private static PassSemantic? ConvertPass(PassBlockModel pass, List<DSLShaderError> errors)
{
var semantic = new PassSemantic
{
name = pass.Name,
hlsl = pass.Hlsl?.Code,
defines = pass.Defines?.Defines,
includes = pass.Includes?.Includes,
localPipeline = ConvertPipeline(pass.LocalPipeline, errors)
};
if (pass.Keywords != null)
{
semantic.keywords = new List<KeywordsGroup>();
foreach (var group in pass.Keywords.Groups)
{
var keywordGroup = new KeywordsGroup
{
space = group.Scope?.ToLower() == "global" ? KeywordSpace.Global : KeywordSpace.Local,
keywords = group.Keywords
};
semantic.keywords.Add(keywordGroup);
}
}
foreach (var entry in pass.ShaderEntries)
{
var entryType = entry.EntryType.ToLower();
var shaderEntry = new ShaderEntryPoint
{
shader = entry.ShaderPath,
entry = entry.EntryPoint
};
switch (entryType)
{
case "mesh" or "ms":
semantic.meshShader = shaderEntry;
break;
case "pixel" or "ps":
semantic.pixelShader = shaderEntry;
break;
case "task" or "ts":
semantic.taskShader = shaderEntry;
break;
default:
errors.Add(new DSLShaderError
{
message = $"Unknown shader entry type '{entry.EntryType}'.",
line = 0,
column = 0
});
break;
}
}
if (semantic.meshShader.shader == null || semantic.pixelShader.shader == null)
{
errors.Add(new DSLShaderError
{
message = $"Pass '{pass.Name}' must contain a mesh/ms shader and a pixel/ps shader declaration.",
line = 0,
column = 0
});
}
return semantic;
}
private class ErrorListener : BaseErrorListener, IAntlrErrorListener<int>, IAntlrErrorListener<IToken>
{
private readonly List<DSLShaderError> _errors;
public ErrorListener(List<DSLShaderError> errors)
{
_errors = errors;
}
public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e)
{
_errors.Add(new DSLShaderError
{
message = msg,
line = line,
column = charPositionInLine
});
}
public new void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e)
{
_errors.Add(new DSLShaderError
{
message = msg,
line = line,
column = charPositionInLine
});
}
}
}

View File

@@ -0,0 +1,78 @@
namespace Ghost.DSL.ShaderParser.Model;
public class ShaderModel
{
public string Name { get; set; } = string.Empty;
public PropertiesBlockModel? Properties { get; set; }
public PipelineBlockModel? Pipeline { get; set; }
public List<PassBlockModel> Passes { get; set; } = new();
public List<FunctionCallModel> FunctionCalls { get; set; } = new();
}
public class PropertiesBlockModel
{
public List<PropertyDeclarationModel> Properties { get; set; } = new();
}
public class PropertyDeclarationModel
{
public string? Scope { get; set; }
public string Type { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public List<string> Initializer { get; set; } = new();
}
public class PipelineBlockModel
{
public Dictionary<string, string> Statements { get; set; } = new();
}
public class PassBlockModel
{
public string Name { get; set; } = string.Empty;
public PipelineBlockModel? LocalPipeline { get; set; }
public DefinesBlockModel? Defines { get; set; }
public IncludesBlockModel? Includes { get; set; }
public KeywordsBlockModel? Keywords { get; set; }
public HlslBlockModel? Hlsl { get; set; }
public List<ShaderEntryModel> ShaderEntries { get; set; } = new();
}
public class DefinesBlockModel
{
public List<string> Defines { get; set; } = new();
}
public class IncludesBlockModel
{
public List<string> Includes { get; set; } = new();
}
public class KeywordsBlockModel
{
public List<KeywordGroupModel> Groups { get; set; } = new();
}
public class KeywordGroupModel
{
public string? Scope { get; set; }
public List<string> Keywords { get; set; } = new();
}
public class HlslBlockModel
{
public string Code { get; set; } = string.Empty;
}
public class ShaderEntryModel
{
public string EntryType { get; set; } = string.Empty; // "mesh", "pixel", "task", etc.
public string ShaderPath { get; set; } = string.Empty;
public string EntryPoint { get; set; } = string.Empty;
}
public class FunctionCallModel
{
public string Name { get; set; } = string.Empty;
public List<string> Arguments { get; set; } = new();
}

View File

@@ -0,0 +1,261 @@
using Antlr4.Runtime.Misc;
using Ghost.DSL.ShaderParser.Model;
namespace Ghost.DSL.ShaderParser;
public class ShaderVisitor : GhostShaderParserBaseVisitor<object>
{
public List<ShaderModel> Shaders { get; } = new();
public override object VisitShaderFile([NotNull] GhostShaderParser.ShaderFileContext context)
{
foreach (var shaderContext in context.shader())
{
var shader = (ShaderModel)VisitShader(shaderContext);
Shaders.Add(shader);
}
return Shaders;
}
public override object VisitShader([NotNull] GhostShaderParser.ShaderContext context)
{
var shader = new ShaderModel
{
Name = StripQuotes(context.STRING_LITERAL().GetText())
};
var shaderBody = context.shaderBody();
if (shaderBody != null)
{
foreach (var propBlock in shaderBody.propertiesBlock())
{
shader.Properties = (PropertiesBlockModel)VisitPropertiesBlock(propBlock);
}
foreach (var pipelineBlock in shaderBody.pipelineBlock())
{
shader.Pipeline = (PipelineBlockModel)VisitPipelineBlock(pipelineBlock);
}
foreach (var passBlock in shaderBody.passBlock())
{
shader.Passes.Add((PassBlockModel)VisitPassBlock(passBlock));
}
foreach (var funcCall in shaderBody.functionCall())
{
shader.FunctionCalls.Add((FunctionCallModel)VisitFunctionCall(funcCall));
}
}
return shader;
}
public override object VisitPropertiesBlock([NotNull] GhostShaderParser.PropertiesBlockContext context)
{
var properties = new PropertiesBlockModel();
foreach (var propDecl in context.propertyDeclaration())
{
properties.Properties.Add((PropertyDeclarationModel)VisitPropertyDeclaration(propDecl));
}
return properties;
}
public override object VisitPropertyDeclaration([NotNull] GhostShaderParser.PropertyDeclarationContext context)
{
var property = new PropertyDeclarationModel
{
Type = context.IDENTIFIER(0).GetText(),
Name = context.IDENTIFIER(1).GetText()
};
if (context.scope() != null)
{
property.Scope = context.scope().GetText();
}
if (context.propertyInitializer() != null)
{
var init = context.propertyInitializer();
foreach (var number in init.NUMBER())
{
property.Initializer.Add(number.GetText());
}
foreach (var identifier in init.IDENTIFIER())
{
property.Initializer.Add(identifier.GetText());
}
}
return property;
}
public override object VisitPipelineBlock([NotNull] GhostShaderParser.PipelineBlockContext context)
{
var pipeline = new PipelineBlockModel();
foreach (var statement in context.pipelineStatement())
{
var key = statement.IDENTIFIER(0).GetText();
var value = statement.IDENTIFIER(1).GetText();
pipeline.Statements[key] = value;
}
return pipeline;
}
public override object VisitPassBlock([NotNull] GhostShaderParser.PassBlockContext context)
{
var pass = new PassBlockModel
{
Name = StripQuotes(context.STRING_LITERAL().GetText())
};
var passBody = context.passBody();
if (passBody != null)
{
foreach (var definesBlock in passBody.definesBlock())
{
pass.Defines = (DefinesBlockModel)VisitDefinesBlock(definesBlock);
}
foreach (var includesBlock in passBody.includesBlock())
{
pass.Includes = (IncludesBlockModel)VisitIncludesBlock(includesBlock);
}
foreach (var keywordsBlock in passBody.keywordsBlock())
{
pass.Keywords = (KeywordsBlockModel)VisitKeywordsBlock(keywordsBlock);
}
foreach (var pipelineBlock in passBody.pipelineBlock())
{
pass.LocalPipeline = (PipelineBlockModel)VisitPipelineBlock(pipelineBlock);
}
foreach (var hlslBlock in passBody.hlslBlock())
{
pass.Hlsl = (HlslBlockModel)VisitHlslBlock(hlslBlock);
}
foreach (var shaderEntry in passBody.shaderEntry())
{
pass.ShaderEntries.Add((ShaderEntryModel)VisitShaderEntry(shaderEntry));
}
}
return pass;
}
public override object VisitDefinesBlock([NotNull] GhostShaderParser.DefinesBlockContext context)
{
var defines = new DefinesBlockModel();
foreach (var defineStmt in context.defineStatement())
{
defines.Defines.Add(defineStmt.IDENTIFIER().GetText());
}
return defines;
}
public override object VisitIncludesBlock([NotNull] GhostShaderParser.IncludesBlockContext context)
{
var includes = new IncludesBlockModel();
foreach (var includeStmt in context.includeStatement())
{
includes.Includes.Add(StripQuotes(includeStmt.STRING_LITERAL().GetText()));
}
return includes;
}
public override object VisitKeywordsBlock([NotNull] GhostShaderParser.KeywordsBlockContext context)
{
var keywords = new KeywordsBlockModel();
foreach (var keywordStmt in context.keywordStatement())
{
var group = new KeywordGroupModel();
if (keywordStmt.scope() != null)
{
group.Scope = keywordStmt.scope().GetText();
}
foreach (var identifier in keywordStmt.IDENTIFIER())
{
group.Keywords.Add(identifier.GetText());
}
keywords.Groups.Add(group);
}
return keywords;
}
public override object VisitHlslBlock([NotNull] GhostShaderParser.HlslBlockContext context)
{
var hlsl = new HlslBlockModel();
// Get the text between the braces
var start = context.LBRACE().Symbol.StopIndex + 1;
var stop = context.RBRACE().Symbol.StartIndex - 1;
if (stop >= start)
{
var input = context.Start.InputStream;
hlsl.Code = input.GetText(new Antlr4.Runtime.Misc.Interval(start, stop));
}
return hlsl;
}
public override object VisitShaderEntry([NotNull] GhostShaderParser.ShaderEntryContext context)
{
var entry = new ShaderEntryModel
{
EntryType = context.IDENTIFIER().GetText(),
ShaderPath = StripQuotes(context.STRING_LITERAL(0).GetText()),
EntryPoint = StripQuotes(context.STRING_LITERAL(1).GetText())
};
return entry;
}
public override object VisitFunctionCall([NotNull] GhostShaderParser.FunctionCallContext context)
{
var funcCall = new FunctionCallModel
{
Name = context.IDENTIFIER().GetText()
};
if (context.functionArguments() != null)
{
foreach (var arg in context.functionArguments().functionArgument())
{
var text = arg.GetText();
if (text.StartsWith('"'))
{
text = StripQuotes(text);
}
funcCall.Arguments.Add(text);
}
}
return funcCall;
}
private static string StripQuotes(string text)
{
if (text.Length >= 2 && text.StartsWith('"') && text.EndsWith('"'))
{
return text.Substring(1, text.Length - 2);
}
return text;
}
}

View File

@@ -0,0 +1,9 @@
using Ghost.Core.Attributes;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: InternalsVisibleTo("Ghost.Editor.Core")]
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
[assembly: InternalsVisibleTo("Ghost.MicroTest")]
[assembly: EngineAssembly]

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="4.7.3" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\ProjectTemplates\Empty.zip">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ghost.Core\Ghost.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
using Ghost.Data.Models;
using System.Text.Json.Serialization;
namespace Ghost.Data;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(TemplateInfo))]
[JsonSerializable(typeof(ProjectMetadata))]
internal partial class JsonContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Ghost.Data.Models;
internal class ProjectInfo
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID
{
get; internal set;
}
public required string Name
{
get; set;
}
public required string MetadataPath
{
get; set;
}
}

View File

@@ -0,0 +1,53 @@
namespace Ghost.Data.Models;
public class ProjectMetadata
{
public const string PROJECT_FILE_EXTENSION_NAME = "gproj";
public Guid ID
{
get; set;
}
public string Name
{
get; set;
}
public Version EngineVersion
{
get; set;
}
public DateTime CreatedAt
{
get; set;
}
public DateTime LastOpened
{
get; set;
}
public ProjectMetadata(string name, Version engineVersion)
{
ID = Guid.NewGuid();
Name = name;
EngineVersion = engineVersion;
CreatedAt = DateTime.UtcNow;
LastOpened = DateTime.UtcNow;
}
// Parameterless constructor for deserialization
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public ProjectMetadata()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
{
}
}
public readonly struct ProjectMetadataInfo(string path, ProjectMetadata metadata)
{
public readonly string Path => path;
public readonly ProjectMetadata Metadata => metadata;
}

View File

@@ -0,0 +1,44 @@
namespace Ghost.Data.Models;
public class TemplateInfo
{
public required string Name
{
get; set;
}
public string? Description
{
get; set;
}
public required Version TemplateVersion
{
get; set;
}
public required Version EngineVersion
{
get; set;
}
}
public struct TemplateData(string templatePath, TemplateInfo info)
{
private const string _ICON_NAME = "icon.png";
private const string _PREVIEW_NAME = "preview.png";
public string directory = Path.GetDirectoryName(templatePath)!;
public readonly TemplateInfo Info => info;
public readonly Uri GetIconURI()
{
return new Uri(Path.Combine(directory, _ICON_NAME));
}
public readonly Uri GetPreviewURI()
{
return new Uri(Path.Combine(directory, _PREVIEW_NAME));
}
}

View File

@@ -0,0 +1,172 @@
using Ghost.Data.Models;
using Ghost.Data.Resources;
using System.Data.SQLite;
namespace Ghost.Data.Repository;
internal static class ProjectRepository
{
private static class Command
{
public const string CONNECTION_STRING = "Data Source={0}\\projects.db;Version=3;";
public const string CREATE_PROJECT_TABLE_STRING = "CREATE TABLE IF NOT EXISTS Projects (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT, MetadataPath TEXT);";
public const string SELECT_PROJECT_STRING = "SELECT * FROM Projects";
public const string INSERT_PROJECT_STRING = "INSERT INTO Projects (Name, MetadataPath) VALUES (@Name, @MetadataPath);";
public const string REMOVE_PROJECT_STRING = "DELETE FROM Projects WHERE ID = @ID;";
public const string UPDATE_PROJECT_STRING = "UPDATE Projects SET Name = @Name, MetadataPath = @MetadataPath WHERE ID = @ID;";
}
private static async Task EnsureTableCreatedAsync(SQLiteConnection connection)
{
using var createCommand = connection.CreateCommand();
createCommand.CommandText = Command.CREATE_PROJECT_TABLE_STRING;
await createCommand.ExecuteNonQueryAsync();
}
public static async IAsyncEnumerable<ProjectInfo> GetAllProjectsAsync()
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING;
using var reader = command.ExecuteReader();
while (await reader.ReadAsync())
{
var project = new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
yield return project;
}
}
public static async Task<ProjectInfo?> GetProjectByIdAsync(int id)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING + " WHERE ID = @ID;";
command.Parameters.AddWithValue("@ID", id);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
}
return null;
}
public static async Task<ProjectInfo?> GetProjectByNameAsync(string name)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING + " WHERE Name = @Name;";
command.Parameters.AddWithValue("@Name", name);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
}
return null;
}
public static async Task<ProjectInfo?> GetProjectByMetadataPathAsync(string metadataPath)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.SELECT_PROJECT_STRING + " WHERE MetadataPath = @MetadataPath;";
command.Parameters.AddWithValue("@MetadataPath", metadataPath);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new ProjectInfo
{
ID = reader.GetInt32(0),
Name = reader.GetString(1),
MetadataPath = reader.GetString(2),
};
}
return null;
}
public static async Task AddProjectAsync(ProjectInfo project)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
command.CommandText = Command.INSERT_PROJECT_STRING;
command.Parameters.AddWithValue("@Name", project.Name);
command.Parameters.AddWithValue("@MetadataPath", project.MetadataPath);
await command.ExecuteNonQueryAsync();
}
public static async Task RemoveProjectAsync(ProjectInfo project)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = Command.REMOVE_PROJECT_STRING;
command.Parameters.AddWithValue("@ID", project.ID);
await command.ExecuteNonQueryAsync();
}
public static async Task UpdateProjectAsync(ProjectInfo project)
{
using var connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, DataPath.s_applicationDataFolder));
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = Command.UPDATE_PROJECT_STRING;
command.Parameters.AddWithValue("@Name", project.Name);
command.Parameters.AddWithValue("@MetadataPath", project.MetadataPath);
command.Parameters.AddWithValue("@ID", project.ID);
await command.ExecuteNonQueryAsync();
}
}

View File

@@ -0,0 +1,8 @@
namespace Ghost.Data.Resources;
public static class AssetsPath
{
public const string ASSETS_FOLDER = "Assets";
public readonly static string s_appIconPath = Path.Combine(AppContext.BaseDirectory, $"{ASSETS_FOLDER}/Icon-256.ico");
}

View File

@@ -0,0 +1,9 @@
namespace Ghost.Data.Resources;
public class DataPath
{
public const string ENGINE_DATA_FOLDER_NAME = "GhostEngine";
public readonly static string s_applicationDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME);
public readonly static string s_projectTemplateFolder = Path.Combine(s_applicationDataFolder, "ProjectTemplates");
}

View File

@@ -0,0 +1,226 @@
using Ghost.Core;
using Ghost.Data.Models;
using Ghost.Data.Repository;
using Ghost.Data.Resources;
using System.IO.Compression;
using System.Text.Json;
namespace Ghost.Data.Services;
internal partial class ProjectService
{
private const string _TEMPLATE_CONTENT_FILE = "content.zip";
public const string ASSETS_FOLDER = "Assets";
public const string CACHE_FOLDER = "Caches";
public const string CONFIG_FOLDER = "Configs";
public static ProjectMetadataInfo CurrentProject
{
get;
set;
}
public static void EnsureDefaultTemplate()
{
var templates = Directory.GetFiles(DataPath.s_projectTemplateFolder, "template.json", SearchOption.AllDirectories);
if (templates.Length > 0)
{
return; // Default template already exists
}
var defaultTemplatePath = Path.Combine(AppContext.BaseDirectory, "Assets/ProjectTemplates/Empty.zip");
ZipFile.ExtractToDirectory(defaultTemplatePath, DataPath.s_projectTemplateFolder, true);
}
public static async IAsyncEnumerable<(string path, TemplateInfo info)> GetProjectTemplatesAsync()
{
var templatesFolder = DataPath.s_projectTemplateFolder;
if (!Directory.Exists(templatesFolder))
{
yield break;
}
var templates = Directory.GetFiles(DataPath.s_projectTemplateFolder, "template.json", SearchOption.AllDirectories);
foreach (var templatePath in templates)
{
var fileStream = File.OpenRead(templatePath);
var templateInfo = await JsonSerializer.DeserializeAsync<TemplateInfo>(fileStream, JsonContext.Default.TemplateInfo);
if (templateInfo == null)
{
continue;
}
yield return (templatePath, templateInfo);
}
}
public static async Task CreateMetadataFileAsync(string path, ProjectMetadata metadata)
{
await using var fileStream = File.Create(path);
await JsonSerializer.SerializeAsync(fileStream, metadata, JsonContext.Default.ProjectMetadata);
}
public static async Task<ProjectMetadata?> LoadMetadataAsync(string ghostprojPath)
{
if (!File.Exists(ghostprojPath))
{
throw new FileNotFoundException("Project metadata file not found.", ghostprojPath);
}
await using var fileStream = File.OpenRead(ghostprojPath);
return await JsonSerializer.DeserializeAsync<ProjectMetadata>(fileStream, JsonContext.Default.ProjectMetadata);
}
public static async Task<Result<ProjectMetadataInfo>> ValidateProjectDirectoryAsync(string? projectDirectory)
{
if (string.IsNullOrWhiteSpace(projectDirectory) || !Directory.Exists(projectDirectory))
{
return Result<ProjectMetadataInfo>.Failure("Project directory is invalid or does not exist.");
}
var projectAssetsPath = Path.Combine(projectDirectory, ASSETS_FOLDER);
var projectConfigPath = Path.Combine(projectDirectory, CONFIG_FOLDER);
if (!Directory.Exists(projectAssetsPath) || !Directory.Exists(projectConfigPath))
{
return Result<ProjectMetadataInfo>.Failure("Project folder structure is invalid.");
}
var metadataPath = Directory.GetFiles(projectDirectory, $"*.{ProjectMetadata.PROJECT_FILE_EXTENSION_NAME}", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (string.IsNullOrWhiteSpace(metadataPath) || !File.Exists(metadataPath))
{
return Result<ProjectMetadataInfo>.Failure("Project metadata file not found.");
}
var metadata = await LoadMetadataAsync(metadataPath);
if (metadata == null)
{
return Result<ProjectMetadataInfo>.Failure("Project metadata file is corrupted or invalid.");
}
return new ProjectMetadataInfo(metadataPath, metadata);
}
private static async ValueTask SetupRequestFolderAsync(string projectDirectory, string templateDirectory)
{
var projectAssetsPath = Path.Combine(projectDirectory, ASSETS_FOLDER);
var projectConfigPath = Path.Combine(projectDirectory, CONFIG_FOLDER);
var templateContentPath = Path.Combine(templateDirectory, _TEMPLATE_CONTENT_FILE);
Directory.CreateDirectory(projectAssetsPath);
if (File.Exists(templateContentPath))
{
await Task.Run(() =>
{
ZipFile.ExtractToDirectory(templateContentPath, projectAssetsPath);
});
}
Directory.CreateDirectory(projectConfigPath);
}
}
internal partial class ProjectService
{
public Task AddProjectAsync(ProjectInfo project)
{
return ProjectRepository.AddProjectAsync(project);
}
public async Task<ProjectInfo> AddProjectAsync(string name, string path)
{
var project = new ProjectInfo
{
Name = name,
MetadataPath = path,
};
await ProjectRepository.AddProjectAsync(project);
return project;
}
public Task RemoveProjectAsync(ProjectInfo project)
{
return ProjectRepository.RemoveProjectAsync(project);
}
public Task UpdateProjectAsync(ProjectInfo project)
{
return ProjectRepository.UpdateProjectAsync(project);
}
public async Task<bool> HasProjectAsync(string path)
{
return await ProjectRepository.GetProjectByMetadataPathAsync(path) != null;
}
public async IAsyncEnumerable<ProjectInfo> GetAllProjectAsync()
{
var badProjectList = new List<ProjectInfo>();
await foreach (var project in ProjectRepository.GetAllProjectsAsync())
{
if (string.IsNullOrWhiteSpace(project.MetadataPath) || !File.Exists(project.MetadataPath))
{
badProjectList.Add(project);
continue;
}
yield return project;
}
foreach (var badProject in badProjectList)
{
await ProjectRepository.RemoveProjectAsync(badProject);
}
}
public async Task<Result<ProjectMetadataInfo>> CreateProjectAsync(string projectName, string projectDirectory, Version engineVersion, string templatePath)
{
try
{
var projectPath = Path.Combine(projectDirectory, projectName);
if (!Directory.Exists(projectPath))
{
Directory.CreateDirectory(projectPath);
}
else
{
// Check if folder is empty
if (Directory.EnumerateFiles(projectPath, "*", SearchOption.AllDirectories).Any())
{
return Result.Failure("Directory is not empty");
}
}
var metadata = new ProjectMetadata(projectName, engineVersion);
var metadataPath = Path.Combine(projectPath, $"{projectName}.{ProjectMetadata.PROJECT_FILE_EXTENSION_NAME}");
await CreateMetadataFileAsync(metadataPath, metadata);
await SetupRequestFolderAsync(projectPath, templatePath);
var info = await AddProjectAsync(projectName, metadataPath);
return Result.Success(new ProjectMetadataInfo(metadataPath, metadata));
}
catch (Exception e)
{
return Result.Failure($"Failed to create project: {e.Message}");
}
}
public async Task<Result<ProjectMetadataInfo>> AddProjectFromDirectoryAsync(string projectDirectory)
{
var result = await ValidateProjectDirectoryAsync(projectDirectory);
if (result.IsFailure)
{
return result;
}
if (await HasProjectAsync(result.Value.Path))
{
return Result.Failure("Project already exists.");
}
await AddProjectAsync(result.Value.Metadata.Name, result.Value.Path);
return result;
}
}

View File

@@ -0,0 +1,7 @@
using Ghost.Core.Attributes;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: EngineAssembly]

View File

@@ -0,0 +1,185 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
namespace Ghost.Editor.Core.AssetHandler;
public abstract class Asset
{
public Guid ID
{
get;
}
public abstract Guid TypeID
{
get;
}
public Guid[] Dependencies
{
get;
}
public IAssetSettings? Settings
{
get;
}
protected Asset(Guid id, Guid[] dependencies, IAssetSettings? settings)
{
ID = id;
Dependencies = dependencies;
Settings = settings;
}
public virtual ValueTask RefreshAsync(IAssetRegistry db, CancellationToken token = default)
{
return ValueTask.CompletedTask;
}
}
// Do not change the order of the fields in this struct, as it is used for binary serialization/deserialization.
[StructLayout(LayoutKind.Sequential, Size = SIZE)]
internal struct AssetMetadata
{
public const int CURRENT_FORMAT_VERSION = 1;
public const int SIZE = 128; // Fixed size for metadata header. We choose 128 bytes to allow future expansion without breaking compatibility.
public AssetMetadata(Guid id, Guid typeID)
{
FormatVersion = CURRENT_FORMAT_VERSION;
ID = id;
TypeID = typeID;
}
public int FormatVersion
{
get;
}
public Guid ID
{
get;
}
public Guid TypeID
{
get;
}
public int HandlerVersion
{
get; set;
}
public int DependencyCount
{
get; set;
}
public long DependenciesOffset
{
get; set;
}
public long SettingsOffset
{
get; set;
}
public long SettingsSize
{
get; set;
}
public long ContentOffset
{
get; set;
}
public long ContentSize
{
get; set;
}
public static void WriteToStream(Stream stream, scoped ref readonly AssetMetadata metadata)
{
var buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in metadata, 1));
stream.Write(buffer);
}
public static AssetMetadata ReadFromStream(Stream stream)
{
Span<byte> buffer = stackalloc byte[SIZE];
stream.ReadExactly(buffer);
return Unsafe.ReadUnaligned<AssetMetadata>(ref MemoryMarshal.GetReference(buffer));
}
}
[StructLayout(LayoutKind.Sequential, Size = SIZE)]
public readonly struct DependencyInfo
{
public const int SIZE = 16;
public Guid ID
{
get; init;
}
public readonly ReadOnlySpan<byte> AsBytes()
{
return MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1));
}
}
public readonly struct AssetReference : IEquatable<AssetReference>
{
private readonly int _value;
/// <summary>
/// The index of the asset in the dependency list.
/// </summary>
public int Index
{
get => Math.Abs(_value) - 1;
}
public static AssetReference Null => default;
public readonly bool IsInternal => _value >= 0;
public readonly bool IsExternal => _value < 0;
public bool Equals(AssetReference other)
{
return _value == other._value;
}
public override int GetHashCode()
{
return _value.GetHashCode();
}
public override bool Equals(object? obj)
{
return obj is AssetReference reference && Equals(reference);
}
public static bool operator ==(AssetReference left, AssetReference right)
{
return left.Equals(right);
}
public static bool operator !=(AssetReference left, AssetReference right)
{
return !(left == right);
}
}
public interface IAssetSettings
{
ValueTask<Result<long>> WriteToStreamAsync(Stream stream, CancellationToken token = default);
ValueTask<Result<IAssetSettings>> ReadFromStreamAsync(Stream stream, CancellationToken token = default);
}

View File

@@ -0,0 +1,66 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.AssetHandler;
[AttributeUsage(AttributeTargets.Class)]
public sealed class CustomAssetHandlerAttribute : Attribute
{
public required string ID
{
get; init;
}
public bool AllowCaching
{
get; init;
} = true;
public required string[] SupportedExtensions
{
get; init;
}
}
public enum DependencyUpdateType
{
Add,
Remove
}
public interface IAssetExportOptions;
public interface IAssetHandler
{
ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default);
ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default);
}
public interface IImportableAssetHandler : IAssetHandler
{
ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default);
ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default);
}
public static class AssetHandlerExtensions
{
public static async ValueTask<Result> ImportAsync(this IImportableAssetHandler handler, string sourceFilePath, string targetFilePath, Guid id, CancellationToken token = default)
{
await using var sourceStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
return await handler.ImportAsync(sourceStream, targetStream, id, token);
}
public static async ValueTask<Result> ExportAsync(this IImportableAssetHandler handler, string assetFilePath, string targetFilePath, IAssetExportOptions? options, CancellationToken token = default)
{
await using var assetStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
await using var targetStream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
return await handler.ExportAsync(assetStream, targetStream, options, token);
}
public static async ValueTask<Result<Asset>> ReadAsync(this IAssetHandler handler, string assetFilePath, IAssetRegistry assetDatabase, CancellationToken token = default)
{
await using var sourceStream = new FileStream(assetFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return await handler.LoadAsync(sourceStream, assetDatabase, token);
}
}

View File

@@ -0,0 +1,378 @@
using Ghost.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.Image;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.AssetHandler;
public enum TextureType : uint
{
Default,
Normal,
Lightmap,
SingleChannel
}
public enum TextureShape : uint
{
Texture2D,
Texture3D,
TextureCube
}
public enum TextureSize : uint
{
Size256 = 256,
Size512 = 512,
Size1024 = 1024,
Size2048 = 2048,
Size4096 = 4096,
Size8192 = 8192
}
public enum TextureCompressionLevel : uint
{
Low,
Normal,
High
}
public enum TextureCompressionEffort : uint
{
Fastest,
Normal,
Production
}
public enum MipmapFilter : uint
{
Box,
Triangle,
Kaiser,
MitchellNetravali
}
public class TextureAsset : Asset
{
internal const string _TYPE_ID = "0906F4EB-C3F0-431B-BCEA-132C88AB0C3F";
internal static readonly Guid s_typeGuid = Guid.Parse(_TYPE_ID);
public override Guid TypeID => s_typeGuid;
public TextureAsset(Guid id, Guid[] dependencies, IAssetSettings? settings)
: base(id, dependencies, settings)
{
}
}
public class TextureAssetSettings : IAssetSettings
{
public struct BasicSettings()
{
public TextureType TextureType
{
get; set;
} = TextureType.Default;
public TextureShape TextureShape
{
get; set;
} = TextureShape.Texture2D;
public int Columns
{
get; set;
} = 1;
public int Rows
{
get; set;
} = 1;
public bool IsSRGB
{
get; set;
} = true;
}
public struct AdvancedSettings()
{
public bool StretchToPowerOfTwo
{
get; set;
} = true;
public bool VirtualTexture
{
get; set;
} = false;
public bool GenerateMipmaps
{
get; set;
} = true;
public uint MipmapLevelCount
{
get; set;
} = 0; // 0 means generate full mipmap levels.
public bool GammaCorrection
{
get; set;
} = true;
public bool PremultiplyAlpha
{
get; set;
} = false;
public MipmapFilter MipmapFilter
{
get; set;
} = MipmapFilter.Kaiser;
public TextureCompressionLevel CompressionLevel
{
get; set;
} = TextureCompressionLevel.Normal;
public TextureCompressionEffort CompressionEffort
{
get; set;
} = TextureCompressionEffort.Normal;
public bool UseBorderColor
{
get; set;
} = false;
public Color32 BorderColor
{
get; set;
} = new Color32(0, 0, 0, 0);
public bool ZeroAlphaBorder
{
get; set;
} = false;
public bool CutoutAlpha
{
get; set;
} = false;
public byte CutoutAlphaThreshold
{
get; set;
} = 127;
public bool ScaleAlphaForMipCoverage
{
get; set;
} = false;
public byte ScaleAlphaForMipCoverageThreshold
{
get; set;
} = 127;
public bool MipmapStreaming
{
get; set;
} = false;
}
public struct SamplerSettings()
{
public TextureSize MaxSize
{
get; set;
} = TextureSize.Size2048;
public TextureFilterMode FilterMode
{
get; set;
} = TextureFilterMode.Anisotropic;
public TextureAddressMode WrapMode
{
get; set;
} = TextureAddressMode.Repeat;
}
public BasicSettings Basic
{
get; set;
} = new BasicSettings();
public AdvancedSettings Advanced
{
get; set;
} = new AdvancedSettings();
public SamplerSettings Sampler
{
get; set;
} = new SamplerSettings();
public async ValueTask<Result<long>> WriteToStreamAsync(Stream stream, CancellationToken token = default)
{
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
var tempArray = ArrayPool<byte>.Shared.Rent(size);
try
{
ref byte address = ref MemoryMarshal.GetReference(tempArray);
Unsafe.WriteUnaligned(ref address, Basic);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>()), Advanced);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()), Sampler);
await stream.WriteAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false);
return Result.Success<long>(size);
}
catch (Exception ex)
{
return Result.Failure($"Failed to write texture asset settings to stream: {ex.Message}");
}
finally
{
ArrayPool<byte>.Shared.Return(tempArray);
}
}
public async ValueTask<Result<IAssetSettings>> ReadFromStreamAsync(Stream stream, CancellationToken token = default)
{
var size = Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>() + Unsafe.SizeOf<SamplerSettings>();
var tempArray = ArrayPool<byte>.Shared.Rent(size);
try
{
ref byte address = ref MemoryMarshal.GetReference(tempArray);
await stream.ReadAsync(tempArray.AsMemory(0, size), token).ConfigureAwait(false);
var basic = Unsafe.ReadUnaligned<BasicSettings>(ref address);
var advanced = Unsafe.ReadUnaligned<AdvancedSettings>(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>()));
var sampler = Unsafe.ReadUnaligned<SamplerSettings>(ref Unsafe.Add(ref address, Unsafe.SizeOf<BasicSettings>() + Unsafe.SizeOf<AdvancedSettings>()));
var settings = new TextureAssetSettings
{
Basic = basic,
Advanced = advanced,
Sampler = sampler
};
return Result.Success<IAssetSettings>(settings);
}
catch (Exception ex)
{
return Result.Failure($"Failed to read texture asset settings from stream: {ex.Message}");
}
finally
{
ArrayPool<byte>.Shared.Return(tempArray);
}
}
}
internal class TextureAssetHandler : IImportableAssetHandler
{
private const int _CURRENT_VERSION = 1;
public ValueTask<Result> ExportAsync(Stream assetStream, Stream targetStream, IAssetExportOptions? options, CancellationToken token = default)
{
throw new NotImplementedException();
}
public async ValueTask<Result> ImportAsync(Stream sourceStream, Stream targetStream, Guid id, CancellationToken token = default)
{
var info = ImageInfo.FromStream(sourceStream);
if (info.BitsPerChannel <= 0)
{
return Result.Failure($"Unsupported image format with {info.BitsPerChannel} bits per channel.");
}
ref byte pData = ref Unsafe.NullRef<byte>();
var imageSize = 0ul;
var isFloat = info.BitsPerChannel > 8;
if (isFloat)
{
using var image = ImageResultFloat.FromStream(sourceStream, info.ColorComponents);
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan()));
imageSize = image.Size;
}
else
{
using var image = ImageResult.FromStream(sourceStream, info.ColorComponents);
pData = ref MemoryMarshal.GetReference(MemoryMarshal.AsBytes(image.AsSpan()));
imageSize = image.Size;
}
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
HandlerVersion = _CURRENT_VERSION,
SettingsOffset = AssetMetadata.SIZE,
};
targetStream.Seek(0, SeekOrigin.Begin);
AssetMetadata.WriteToStream(targetStream, ref header);
targetStream.Seek(header.SettingsOffset, SeekOrigin.Begin);
var settings = new TextureAssetSettings();
var sizeResult = await settings.WriteToStreamAsync(targetStream, token).ConfigureAwait(false);
if (sizeResult.IsFailure)
{
return Result.Failure($"Failed to write texture asset settings: {sizeResult.Message}");
}
header.SettingsSize = sizeResult.Value;
header.ContentOffset = header.SettingsOffset + sizeResult.Value;
header.ContentSize = (long)imageSize;
targetStream.Seek(header.ContentOffset, SeekOrigin.Begin);
var offset = 0;
var tempArray = ArrayPool<byte>.Shared.Rent((int)Math.Min(imageSize, 40960ul));
var remaining = imageSize;
try
{
while (remaining > 0)
{
var chunkSize = (int)Math.Min(remaining, (ulong)tempArray.Length);
Unsafe.CopyBlockUnaligned(ref tempArray[0], ref Unsafe.Add(ref pData, offset), (uint)chunkSize);
await targetStream.WriteAsync(tempArray.AsMemory(0, chunkSize), token).ConfigureAwait(false);
offset += chunkSize;
remaining -= (ulong)chunkSize;
}
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to write texture asset content to stream: {ex.Message}");
}
finally
{
ArrayPool<byte>.Shared.Return(tempArray);
}
}
public ValueTask<Result<Asset>> LoadAsync(Stream sourceStream, IAssetRegistry assetDatabase, CancellationToken token = default)
{
throw new NotImplementedException();
}
public ValueTask<Result> SaveAsync(Asset asset, Stream targetStream, IAssetRegistry assetDatabase, CancellationToken token = default)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,102 @@
namespace Ghost.Editor.Core;
/// <summary>
/// The base class for all attributes that can be discovered via <see cref="Utilities.TypeCache"/>.
/// </summary>
public abstract class DiscoverableAttributeBase : Attribute;
[AttributeUsage(AttributeTargets.Method)]
public class AssetOpenHandlerAttribute : DiscoverableAttributeBase
{
public string[] Extensions
{
get;
}
public AssetOpenHandlerAttribute(params string[] extensions)
{
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class AssetImporterAttribute : DiscoverableAttributeBase
{
public string[] SupportedExtensions
{
get;
}
public AssetImporterAttribute(params string[] supportedExtensions)
{
SupportedExtensions = supportedExtensions;
}
}
[AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute : DiscoverableAttributeBase
{
internal Type TargetType
{
get;
}
public CustomEditorAttribute(Type targetType)
{
TargetType = targetType;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class EditorInjectionAttribute : DiscoverableAttributeBase
{
public enum ServiceLifetime
{
Singleton,
Transient,
Scoped
}
public ServiceLifetime Lifetime
{
get;
}
public Type? ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type? implementationType = null)
{
Lifetime = lifetime;
ImplementationType = implementationType;
}
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase
{
public string Tag
{
get;
}
public string Name
{
get;
}
public int Group
{
get;
}
public ContextMenuItemAttribute(string tag, string name, int group = 0)
{
Tag = tag;
Name = name;
Group = group;
}
}

View File

@@ -0,0 +1,49 @@
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
namespace Ghost.Editor.Core.Contracts;
public enum AssetChangeType
{
None = 0,
Created,
Deleted,
Modified,
Renamed,
}
public sealed class AssetChangedEventArgs : EventArgs
{
public string AssetPath
{
get;
}
public string? OldAssetPath
{
get;
}
public AssetChangeType ChangeType
{
get;
}
internal AssetChangedEventArgs(string assetPath, string? oldAssetPath, AssetChangeType changeType)
{
AssetPath = assetPath;
OldAssetPath = oldAssetPath;
ChangeType = changeType;
}
}
public interface IAssetRegistry : IDisposable
{
string? GetAssetPath(Guid id);
Guid GetAssetGuid(string assetPath);
ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default);
ValueTask<Result> ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default);
ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default);
ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default);
}

View File

@@ -0,0 +1,13 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Contracts;
public interface IInspectable
{
public IconSource? CreateIcon();
public UIElement? CreateHeader();
public UIElement? CreateInspector();
}

View File

@@ -0,0 +1,32 @@
namespace Ghost.Editor.Core.Contracts;
public class InspectorSelectionChangedEventArgs : EventArgs
{
public object? Source
{
get;
}
public IInspectable? Selected
{
get;
}
public InspectorSelectionChangedEventArgs(object? source, IInspectable? selected)
{
Source = source;
Selected = selected;
}
}
public interface IInspectorService
{
IInspectable? Selected
{
get;
}
event EventHandler<InspectorSelectionChangedEventArgs> OnSelectionChanged;
void SetSelected(IInspectable? inspectable, object? source);
}

View File

@@ -0,0 +1,7 @@
namespace Ghost.Editor.Core.Contracts;
public interface INavigationAware
{
public void OnNavigatedTo(object? parameter);
public void OnNavigatedFrom();
}

View File

@@ -0,0 +1,10 @@
using CommunityToolkit.WinUI.Behaviors;
using Ghost.Editor.Core.Notifications;
namespace Ghost.Editor.Core.Contracts;
public interface INotificationService
{
public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null);
public void ShowNotification(Notification notification);
}

View File

@@ -0,0 +1,12 @@
namespace Ghost.Editor.Core.Contracts;
public enum IconSize
{
Small,
Large
}
public interface IPreviewService
{
string GetIconPath(string path, bool isDirectory, IconSize size);
}

View File

@@ -0,0 +1,9 @@
namespace Ghost.Editor.Core.Contracts;
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();
}

View File

@@ -0,0 +1,69 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Misaki.HighPerformance.Mathematics;
namespace Ghost.Editor.Core.Controls;
[TemplatePart(Name = "XComponent", Type = typeof(NumberBox))]
[TemplatePart(Name = "YComponent", Type = typeof(NumberBox))]
[TemplatePart(Name = "ZComponent", Type = typeof(NumberBox))]
public sealed partial class Float3Field : ValueControl<float3>
{
private NumberBox? _xComponent;
private NumberBox? _yComponent;
private NumberBox? _zComponent;
public Float3Field()
{
DefaultStyleKey = typeof(Float3Field);
}
protected override void ValueChanged(float3 oldValue, float3 newValue)
{
SyncFromValue();
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_xComponent?.ValueChanged -= OnComponentChanged;
_yComponent?.ValueChanged -= OnComponentChanged;
_zComponent?.ValueChanged -= OnComponentChanged;
_xComponent = GetTemplateChild("XComponent") as NumberBox;
_yComponent = GetTemplateChild("YComponent") as NumberBox;
_zComponent = GetTemplateChild("ZComponent") as NumberBox;
SyncFromValue();
_xComponent?.ValueChanged += OnComponentChanged;
_yComponent?.ValueChanged += OnComponentChanged;
_zComponent?.ValueChanged += OnComponentChanged;
}
private void SyncFromValue()
{
SuppressChangedEvent = true;
_xComponent?.Value = Value.x;
_yComponent?.Value = Value.y;
_zComponent?.Value = Value.z;
SuppressChangedEvent = false;
}
private void OnComponentChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
{
if (SuppressChangedEvent)
{
return;
}
var newValue = new float3(
(float)(_xComponent?.Value ?? 0),
(float)(_yComponent?.Value ?? 0),
(float)(_zComponent?.Value ?? 0));
RiseChangedEvent(Value, newValue);
Value = newValue;
}
}

View File

@@ -0,0 +1,41 @@
<?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.Core.Controls">
<Style TargetType="local:Float3Field">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:Float3Field">
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="X" />
<NumberBox x:Name="XComponent" Grid.Column="1" />
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
Text="Y" />
<NumberBox x:Name="YComponent" Grid.Column="3" />
<TextBlock
Grid.Column="4"
VerticalAlignment="Center"
Text="Z" />
<NumberBox x:Name="ZComponent" Grid.Column="5" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,143 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using System.Reflection;
using Windows.Globalization.NumberFormatting;
namespace Ghost.Editor.Core.Controls;
public sealed partial class PropertyField : ContentControl
{
private static readonly Dictionary<Type, DependencyProperty> _valueProperties = new()
{
{ typeof(TextBox), TextBox.TextProperty },
{ typeof(NumberBox), NumberBox.ValueProperty },
{ typeof(ToggleButton), ToggleButton.IsCheckedProperty },
{ typeof(ToggleSwitch), ToggleSwitch.IsOnProperty },
{ typeof(ComboBox), Selector.SelectedValueProperty },
{ typeof(RangeBase), RangeBase.ValueProperty },
};
private object? _sourceObject;
internal FieldInfo? _propertyInfo;
internal Type? _fieldType;
private object? _lastValue;
public event Action<PropertyField>? OnValueChanged;
public string Label
{
get => (string)GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
nameof(Label),
typeof(string),
typeof(PropertyField),
new PropertyMetadata(default(string)));
public PropertyField()
{
DefaultStyleKey = typeof(PropertyField);
}
private static DependencyProperty? GetValueProperty(Type? fieldType)
{
while (fieldType != null)
{
if (_valueProperties.TryGetValue(fieldType, out var dp))
{
return dp;
}
fieldType = fieldType.BaseType;
}
return null;
}
private static TField ConfigureField<TField>(PropertyField propertyField, FieldInfo fieldInfo, object sourceObject, Func<TField> factory)
where TField : FrameworkElement
{
propertyField._sourceObject = sourceObject;
propertyField._propertyInfo = fieldInfo;
propertyField._fieldType = typeof(TField);
var field = factory();
var dp = GetValueProperty(typeof(TField));
field.SetBinding(dp, new Binding
{
Source = sourceObject,
Path = new PropertyPath(fieldInfo.Name),
Mode = BindingMode.TwoWay,
});
field.RegisterPropertyChangedCallback(dp, (s, e) =>
{
propertyField.OnValueChanged?.Invoke(propertyField);
});
return field;
}
public static PropertyField Create(string label, FieldInfo fieldInfo, object sourceObject)
{
var propertyField = new PropertyField
{
Label = label
};
FrameworkElement content = fieldInfo.FieldType switch
{
Type t when t == typeof(string) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new TextBox()),
Type t when t == typeof(int) || t == typeof(float) || t == typeof(double) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new NumberBox
{
SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Hidden,
AcceptsExpression = true,
NumberFormatter = new DecimalFormatter
{
FractionDigits = t == typeof(int) ? 0 : 9,
}
}),
Type t when t == typeof(bool) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new ToggleSwitch()),
Type t when t == typeof(Enum) => ConfigureField(propertyField, fieldInfo, sourceObject, () => new ComboBox
{
ItemsSource = Enum.GetValues(t),
SelectedValuePath = "Value",
}),
_ => new TextBlock
{
Text = $"Unsupported type: {fieldInfo.FieldType.Name}",
VerticalAlignment = VerticalAlignment.Center,
Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Red)
},
};
propertyField.Content = content;
return propertyField;
}
public void UpdateValue()
{
if (_sourceObject == null || _propertyInfo == null || _fieldType == null)
{
return;
}
var currentValue = _propertyInfo.GetValue(_sourceObject);
if (Equals(currentValue, _lastValue))
{
return;
}
var dp = GetValueProperty(_fieldType);
if (dp != null)
{
SetValue(dp, _propertyInfo.GetValue(_sourceObject));
_lastValue = currentValue;
}
}
}

View File

@@ -0,0 +1,33 @@
<?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.Core.Controls">
<Style TargetType="local:PropertyField">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:PropertyField">
<Grid Height="32" Margin="2,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="125" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="0,0,0,4"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{TemplateBinding Label}"
TextTrimming="CharacterEllipsis" />
<ContentPresenter
Grid.Column="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core.Controls;
public partial class ControlsDictionary : ResourceDictionary
{
private const string _DICTIONARY_PATH = "ms-appx:///Ghost.Editor.Core/Controls/ControlsDictionary.xaml";
public ControlsDictionary()
{
Source = new Uri(_DICTIONARY_PATH, UriKind.Absolute);
}
}

View File

@@ -0,0 +1,9 @@
<?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">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/PropertyField.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/Float3Field.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/Internal/ComponentView.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,157 @@
using Ghost.Editor.Core.Inspector;
using Ghost.Editor.Core.Resources;
using Ghost.Editor.Core.Utilities;
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Controls;
internal sealed unsafe partial class ComponentView : Control
{
private delegate void EditorUpdate();
private StackPanel? _contentContainer;
private readonly World? _world;
private readonly Entity _entity = Entity.Invalid;
private readonly Type? _componentType;
private readonly ComponentInfo _componentInfo;
private object? _managedInstance;
private void* _pComponentData;
private ComponentEditor? _customEditor;
private PropertyField[]? _propertyFields;
private EditorUpdate? _editorUpdate;
public string HeaderText
{
get => (string)GetValue(HeaderTextProperty);
set => SetValue(HeaderTextProperty, value);
}
public static readonly DependencyProperty HeaderTextProperty =
DependencyProperty.Register(nameof(HeaderText), typeof(string), typeof(ComponentView), new PropertyMetadata(string.Empty));
internal ComponentView()
{
DefaultStyleKey = typeof(ComponentView);
Unloaded += (s, e) =>
{
_customEditor?.Destroy();
_contentContainer = null;
_customEditor = null;
_propertyFields = null;
};
}
public ComponentView(string header, World world, Entity entity, Type componentType) : this()
{
HeaderText = header;
_world = world;
_entity = entity;
_componentType = componentType;
_componentInfo = ComponentRegistry.GetComponentInfo(componentType);
}
protected override void OnApplyTemplate()
{
_contentContainer = (StackPanel)GetTemplateChild("ContentContainer");
base.OnApplyTemplate();
ReBuild();
}
private void ReflectionUpdate()
{
if (_propertyFields == null)
{
return;
}
foreach (var propertyField in _propertyFields)
{
propertyField.UpdateValue();
}
}
private void CustomEditorUpdate()
{
_customEditor?.Update();
}
public void ReBuild()
{
if (_contentContainer == null)
{
return;
}
_contentContainer.Children.Clear();
if (_world == null || _componentType == null || _entity == Entity.Invalid)
{
return;
}
if (_propertyFields != null)
{
foreach (var propertyField in _propertyFields)
{
propertyField.OnValueChanged -= OnPropertyValueChanged;
}
}
var componentObject = new ComponentObject(_world, _entity);
var editorType = TypeCache.GetTypes().FirstOrDefault(t =>
typeof(ComponentEditor).IsAssignableFrom(t) &&
t.GetCustomAttribute<CustomEditorAttribute>()?.TargetType.IsAssignableFrom(_componentType) == true);
if (editorType != null)
{
_customEditor = (ComponentEditor)Activator.CreateInstance(editorType)!;
_customEditor.Initialize(componentObject);
_customEditor.Create(_contentContainer);
}
else
{
var fields = _componentType.GetFields(StaticResource.ComponentPropertyBindingFlags);
_propertyFields = new PropertyField[fields.Length];
_pComponentData = _world.EntityManager.GetComponent(_entity, _componentInfo.id);
_managedInstance = Marshal.PtrToStructure((nint)_pComponentData, _componentType);
if (_managedInstance == null)
{
return;
}
for (var i = 0; i < fields.Length; i++)
{
var field = fields[i];
var propertyField = PropertyField.Create(field.Name, field, _managedInstance);
propertyField.OnValueChanged += OnPropertyValueChanged;
_propertyFields[i] = propertyField;
_contentContainer.Children.Add(propertyField);
}
}
_editorUpdate = _customEditor == null ? ReflectionUpdate : CustomEditorUpdate;
_editorUpdate();
}
private void OnPropertyValueChanged(PropertyField field)
{
if (_managedInstance == null || _pComponentData == null)
{
return;
}
Marshal.StructureToPtr(_managedInstance, (nint)_pComponentData, false);
}
}

View File

@@ -0,0 +1,27 @@
<?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.Core.Controls">
<Style TargetType="local:ComponentView">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ComponentView">
<StackPanel Margin="0,0,0,16">
<Border
Padding="8"
HorizontalAlignment="Stretch"
Background="{ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{TemplateBinding HeaderText}" />
</Border>
<StackPanel
x:Name="ContentContainer"
Margin="8,2,2,0"
Spacing="2" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,42 @@
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Controls;
public partial class NavigationTabPage : TabViewItem, INavigationAware
{
public virtual void OnNavigatedTo(object? parameter)
{
}
public virtual void OnNavigatedFrom()
{
}
}
public sealed partial class NavigationTabView : TabView
{
public NavigationTabView()
{
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment = VerticalAlignment.Stretch;
SelectionChanged += NavigationTabView_SelectionChanged;
}
private void NavigationTabView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var oldItem in e.RemovedItems)
{
if (oldItem is NavigationTabPage oldPage)
{
oldPage.OnNavigatedFrom();
}
}
if (SelectedItem is NavigationTabPage newPage)
{
newPage.OnNavigatedTo(null);
}
}
}

View File

@@ -0,0 +1,215 @@
using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Xaml.Controls;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Controls;
public sealed partial class ContextFlyout : MenuFlyout
{
private class MenuNode
{
public required string Name
{
get; init;
}
public MethodInfo? Method
{
get; set;
}
public List<MenuNode> Children
{
get;
} = new();
public int RawGroup
{
get; set;
} = int.MaxValue;
// The calculated group used for sorting (min of children for folders)
public int EffectiveGroup
{
get; set;
}
}
private bool _isPopulated;
public string Tag
{
get; set;
} = string.Empty;
public ContextFlyout()
{
Opening += ContextFlyout_Opening;
}
// Recursively sorts nodes and calculates folder groups
private static void PrepareNodes(List<MenuNode> nodes)
{
if (nodes.Count == 0)
{
return;
}
foreach (var node in nodes)
{
if (node.Children.Count > 0)
{
// Go deep first
PrepareNodes(node.Children);
// A folder's group is determined by its highest priority child (lowest group number).
// This ensures a "File" folder (containing Group 0 items) sits at the top
// alongside other Group 0 leaf items.
node.EffectiveGroup = node.Children.Min(c => c.EffectiveGroup);
}
else
{
node.EffectiveGroup = node.RawGroup;
}
}
// Sort by Group, then by Name
nodes.Sort((a, b) =>
{
var groupCompare = a.EffectiveGroup.CompareTo(b.EffectiveGroup);
return groupCompare != 0
? groupCompare
: string.CompareOrdinal(a.Name, b.Name);
});
}
// Recursively builds the UI elements
private static void BuildNodes(List<MenuNode> nodes, IList<MenuFlyoutItemBase> targetCollection)
{
if (nodes.Count == 0)
{
return;
}
int currentGroup = nodes[0].EffectiveGroup;
foreach (var node in nodes)
{
if (node.EffectiveGroup != currentGroup)
{
targetCollection.Add(new MenuFlyoutSeparator());
currentGroup = node.EffectiveGroup;
}
if (node.Children.Count > 0)
{
var subItem = new MenuFlyoutSubItem
{
Text = node.Name
};
// Recursively render children into the subitem
BuildNodes(node.Children, subItem.Items);
targetCollection.Add(subItem);
}
else
{
var menuItem = new MenuFlyoutItem
{
Text = node.Name
};
var methodToInvoke = node.Method;
menuItem.Click += (_, _) =>
{
methodToInvoke?.Invoke(null, null);
};
targetCollection.Add(menuItem);
}
}
}
private void PopulateContextMenu()
{
var methods = TypeCache.GetMethodsWithAttribute<ContextMenuItemAttribute>();
if (methods == null)
{
return;
}
// 1. Build the Tree
var rootNodes = new List<MenuNode>();
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<ContextMenuItemAttribute>();
if (attr == null)
{
continue;
}
// Filter tags
if (!string.Equals(attr.Tag, Tag, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var nameSpan = attr.Name.AsSpan();
var pathParts = nameSpan.Split('/');
var currentLevel = rootNodes;
MenuNode? currentNode = null;
foreach (var range in pathParts)
{
var part = nameSpan[range.Start..range.End];
MenuNode? foundNode = null;
// Try to find existing node in the current level
foreach (var node in currentLevel)
{
if (part.Equals(node.Name.AsSpan(), StringComparison.Ordinal))
{
foundNode = node;
break;
}
}
if (foundNode == null)
{
foundNode = new MenuNode { Name = part.ToString() };
currentLevel.Add(foundNode);
}
currentNode = foundNode;
// If this is the last part, it's the executable item
if (range.End.Value == nameSpan.Length)
{
currentNode.Method = method;
currentNode.RawGroup = attr.Group;
}
currentLevel = currentNode.Children;
}
}
PrepareNodes(rootNodes);
BuildNodes(rootNodes, Items);
}
private async void ContextFlyout_Opening(object? sender, object e)
{
if (_isPopulated)
{
return;
}
PopulateContextMenu();
_isPopulated = true;
}
}

View File

@@ -0,0 +1,70 @@
using Ghost.Editor.Core.Event;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Controls;
public partial class ValueControl<T> : Control
{
private bool _suppressChangedEvent;
protected bool SuppressChangedEvent
{
get => _suppressChangedEvent;
set => _suppressChangedEvent = value;
}
public T Value
{
get => (T)GetValue(ValueProperty);
set
{
if (EqualityComparer<T>.Default.Equals(Value, value))
{
return;
}
SetValue(ValueProperty, value);
}
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(T), typeof(ValueControl<T>), new PropertyMetadata(default(T), ChangedCallback));
public event ValueChangedEventHandler<T>? OnValueChanged;
private static void ChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ValueControl<T> valueControl)
{
valueControl.ValueChanged((T)e.OldValue, (T)e.NewValue);
if (!valueControl._suppressChangedEvent)
{
valueControl.OnValueChanged?.Invoke(valueControl, new((T)e.OldValue, (T)e.NewValue));
}
}
}
protected virtual void ValueChanged(T oldValue, T newValue)
{
}
protected void RiseChangedEvent(T oldValue, T newValue)
{
OnValueChanged?.Invoke(this, new(oldValue, newValue));
}
/// <summary>
/// Sets the _value without notifying the change event.
/// </summary>
/// <param name="value">The new _value to set.</param>
/// <remarks>This method only suppresses the change event notification, not the <see cref="ValueChanged(T, T)"/> method.
/// Useful when you need to change the _value programmatically without triggering the change event.</remarks>
public void SetValueWithoutNotifying(T value)
{
_suppressChangedEvent = true;
SetValue(ValueProperty, value);
_suppressChangedEvent = false;
}
}

View File

@@ -0,0 +1,65 @@
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core;
public static class EditorApplication
{
public const string ASSETS_FOLDER_NAME = "Assets";
public const string CACHES_FOLDER_NAME = "Caches";
private static IServiceProvider? s_serviceProvider;
private static string s_currentProjectPath = string.Empty;
private static string s_currentProjectName = string.Empty;
private static DispatcherQueue? s_dispatcherQueue;
internal static Application CurrentApplication => Application.Current;
internal static string CurrentProjectPath => s_currentProjectPath;
internal static string CurrentProjectName => s_currentProjectName;
public static DispatcherQueue DispatcherQueue
{
get
{
if (s_dispatcherQueue is null)
{
throw new InvalidOperationException("DispatcherQueue is not initialized.");
}
return s_dispatcherQueue;
}
}
internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName)
{
s_serviceProvider = serviceProvider;
s_currentProjectPath = projectPath;
s_currentProjectName = projectName;
}
internal static void SetDispatcherQueue(DispatcherQueue dispatcherQueue)
{
s_dispatcherQueue = dispatcherQueue;
}
public static T GetService<T>()
where T : class
{
if (s_serviceProvider?.GetService(typeof(T)) is not T service)
{
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
internal static void Shutdown()
{
if (s_serviceProvider?.GetService(typeof(IAssetService)) is AssetHandle.AssetService assetService)
{
assetService.Shutdown();
}
}
}

View File

@@ -0,0 +1,22 @@
namespace Ghost.Editor.Core.Event;
public delegate void ValueChangedEventHandler<T>(object? sender, ValueChangedEventArgs<T> args);
public class ValueChangedEventArgs<T> : EventArgs
{
public T OldValue
{
get;
}
public T NewValue
{
get;
}
public ValueChangedEventArgs(T oldValue, T newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Ghost.Editor.Core</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<!-- 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>
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Runtime\Ghost.Core\Ghost.Core.csproj" />
<ProjectReference Include="..\..\Runtime\Ghost.Engine\Ghost.Engine.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\BasicInput\PropertyField.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\BasicInput\Vector3Field.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\Internal\ComponentView.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Inspector;
public abstract class ComponentEditor
{
private ComponentObject _componentObject;
/// <summary>
/// Represents the underlying component object used by this class to manage its functionality.
/// </summary>
protected ComponentObject ComponentObject => _componentObject;
internal void Initialize(ComponentObject componentObject)
{
_componentObject = componentObject;
}
/// <summary>
/// Called when the component editor is created.
/// </summary>
/// <param name="container">The container to add the editor controls to.</param>
public virtual void Create(StackPanel container)
{
}
/// <summary>
/// Called when the component editor needs to update its UI based on the current state of the component data.
/// </summary>
public virtual void Update()
{
}
/// <summary>
/// Called when the component editor is destroyed.
/// </summary>
public virtual void Destroy()
{
}
}

View File

@@ -0,0 +1,27 @@
using Ghost.Entities;
namespace Ghost.Editor.Core.Inspector;
public readonly struct ComponentObject
{
private readonly World _world;
private readonly Entity _entity;
internal ComponentObject(World world, Entity entity)
{
_world = world;
_entity = entity;
}
public ref T GetData<T>()
where T : unmanaged, IComponent
{
return ref _world.EntityManager.GetComponent<T>(_entity);
}
public void SetData<T>(in T data)
where T : unmanaged, IComponent
{
_world.EntityManager.SetComponent(_entity, data);
}
}

View File

@@ -0,0 +1,9 @@
namespace Ghost.Editor.Core.Notifications;
public enum MessageType
{
Informational,
Success,
Warning,
Error
}

View File

@@ -0,0 +1,18 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Resources;
public static class EditorIconSource
{
public static readonly IconSource scene_24 = new FontIconSource
{
Glyph = "\uF159",
FontSize = 24
};
public static readonly IconSource entity_24 = new FontIconSource
{
Glyph = "\uF158",
FontSize = 24
};
}

View File

@@ -0,0 +1,8 @@
using System.Reflection;
namespace Ghost.Editor.Core.Resources;
internal static class StaticResource
{
public static readonly BindingFlags ComponentPropertyBindingFlags = BindingFlags.Public | BindingFlags.Instance;
}

View File

@@ -0,0 +1,45 @@
using Ghost.Entities;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class EntityNode : SceneGraphNode
{
private readonly Entity _entity;
public Entity Entity => _entity;
public override IconSource? CreateIcon()
{
return new FontIconSource
{
Glyph = "\uF158"
};
}
public override UIElement? CreateHeader()
{
return null;
}
public override UIElement? CreateInspector()
{
throw new NotImplementedException();
}
public override DataTemplate GetSceneHierarchyTemplate()
{
var template = @"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:sg=""using:Ghost.Editor.Core.SceneGraph"" 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=""&#xF158;"" />
<TextBlock Margin=""5,0,0,0"" Text=""{x:Bind Name, Mode=OneWay}"" />
</StackPanel>
</TreeViewItem>
</DataTemplate>";
return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
}
}

View File

@@ -0,0 +1,87 @@
# Architecture Plan: Scene Graph and Scene Representation
The Scene Graph is a hierarchical structure that represents all the objects and entities within a 3D scene in the Ghost Editor.
## Scene Graph (Editor representation of runtime data)
There should be three main types of nodes in the Scene Graph for now:
1. **Scene Graph Node**: The base class for all nodes in the Scene Graph.
2. **Entity Node**: Represents an individual entity within a scene. Name stored here, not runtime component.
3. **Scene Node**: Represents a Scene object, which can contain multiple entities. Name stored here not runtime data.
### Editor World
Editor contains a different world compares to the runtime world. When user click the Play button, we will create a runtime world and load the scene data from the editor world to the runtime world.
This allows us to
1. Unload the runtime only systems like physics, rendering, etc when user stop playing.
2. Load editor only systems like gizmos, debug, etc when user stop playing.
3. Allow editor only entities like editor camera, editor lights, etc to exist in the editor world without affecting the runtime world.
### Editor Hierarchy
The Scene Graph should be represented as a tree structure in the editor (TreeView in WinUI 3), where:
- The top level nodes represents the loaded Scenes in the editor world.
- Levels below the Scene nodes represents the Entity nodes that belong to that scene.
- Each Entity node can have child Entity nodes representing parent-child relationships between entities.
An example hierarchy could look like this:
```
- Scene 1
- Entity A
- Entity B
- Entity C
- Scene 2
- Entity D
```
## Scene (The runtime representation)
A Scene is a collection of entities with SceneID component from a world that are grouped together. There can be multiple scenes in a world.
### Save a Scene
When save a scene, all entities with the SceneID component matching the scene's ID should be included in the saved data.
When an Entity references another Entity in the same scene, we should store the file local id instead of the global entity id.
For example, if Entity A (id: 10, 5th in scene) references Entity B (id: 20, 50th in scene) in the same scene, in the saved data for Entity A,
we should store 50 (the file local id) as the reference to Entity B instead of 20 (the global entity id).
> We does not allow cross-scene references for now because ideally it's not a good practice to have cross-scene references.
> We can use query or singleton pattern to access entities from other scenes if needed because they are in the same world.
### Load a Scene
When loading a scene, we need to reconstruct the entities and their relationships based on the saved data.
1. We allocate the entities in the world and assign them new global entity IDs.
2. We remap the file local IDs to the new global entity IDs and change the references accordingly.
For example if Entity A (file local id: 5) references Entity B (file local id: 50) in the saved data,
we need to find the new global entity IDs for both entities after loading and update the reference in Entity A to point to the new global entity ID of Entity B.
### Data format
The scene data should be stored in a structured format (JSON and binary) that includes:
- List of entities with their components and properties (Entities must in the order that file local id directly maps to the index in the list)
- References between entities using file local IDs
> The name of the saved scene file should match the name of the scene node in the editor.
JSON should only be used in the editor and JSON serialization/deserialization logic should also only exist in the editor codebase (Ghost.Editor.Core). Reflection is allowed here.
Binary format should be used in the runtime for better performance. The runtime codebase (Ghost.Engine) must be aot compatible.
Currently we strict the IComponent to must be unmanaged and blittable types.
However, we also support ManagedEntity and ManagedEntityRef with ScriptComponent to allow OOP like logic for common gameplay logic that DOD pattern is not suitable for.
Serializing/deserializing with those components will be tricky. We can use MemoryPack (already installed) for binary serialization/deserialization because it supports both unmanaged and managed types.
## What need to implement
- [ ] Scene type for the runtime representation if needed
- [ ] Scene Graph data structures (SceneNode, EntityNode)
- [ ] Editor World management (loading/unloading scenes, managing entities)
- [ ] Scene saving/loading logic with file local ID remapping
- [ ] Serialization/deserialization logic for scene data (JSON for editor, binary for runtime)
- [ ] UI integration for displaying and managing the Scene Graph in the editor with WinUI 3 TreeView

View File

@@ -0,0 +1,27 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Core.SceneGraph;
public abstract partial class SceneGraphNode : ObservableObject, IInspectable
{
[ObservableProperty]
public partial string Name
{
get; set;
}
public ObservableCollection<SceneGraphNode> Children
{
get;
} = new();
public abstract IconSource? CreateIcon();
public abstract UIElement? CreateHeader();
public abstract UIElement? CreateInspector();
public abstract DataTemplate GetSceneHierarchyTemplate();
}

View File

@@ -0,0 +1,45 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.SceneGraph;
public sealed partial class SceneNode : SceneGraphNode
{
public override IconSource? CreateIcon()
{
return new FontIconSource
{
Glyph = "\uF156"
};
}
// TODO: Implement custom header and inspector UI for the SceneNode
public override UIElement? CreateHeader()
{
return null;
}
public override UIElement? CreateInspector()
{
return null;
}
public override DataTemplate GetSceneHierarchyTemplate()
{
var template = @"
<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" xmlns:sg=""using:Ghost.Editor.Core.SceneGraph"" 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=""&#xF156;""/>
<TextBlock Margin=""10,0"" Text=""{ x:Bind Name, Mode=OneWay}""/>
</StackPanel>
</TreeViewItem>
</DataTemplate>";
return (DataTemplate)Microsoft.UI.Xaml.Markup.XamlReader.Load(template);
}
}

View File

@@ -0,0 +1,6 @@
namespace TestProject.AssetDB;
internal partial class AssetRegistry
{
// TODO: Sqlite backend implementation
}

View File

@@ -0,0 +1,510 @@
using Ghost.Core;
using Ghost.Editor.Core.AssetHandler;
using Ghost.Editor.Core.Contracts;
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace TestProject.AssetDB;
internal class PathComparer : IEqualityComparer<string>
{
private static string ToCanonicalPath(string? path)
{
return path?.Replace('\\', '/').TrimEnd('/') ?? string.Empty;
}
public bool Equals(string? x, string? y)
{
return string.Equals(
ToCanonicalPath(x),
ToCanonicalPath(y),
StringComparison.Ordinal);
}
public int GetHashCode(string str)
{
return ToCanonicalPath(str).GetHashCode(StringComparison.Ordinal);
}
}
// TODO: Path based locking for multi-threaded access?
// Is it actually necessary since this is mostly used in editor environment where single-threaded access is common (99.999%)?
internal partial class AssetRegistry : IAssetRegistry
{
public const string ASSET_EXTENSION = ".gasset";
public const string TEMP_EXTENSION = ".gtemp";
private readonly string _rootDirectory;
private readonly FileSystemWatcher _watcher;
private readonly ConcurrentDictionary<string, Guid> _pathToGuid;
private readonly ConcurrentDictionary<Guid, string> _guidToPath;
private readonly ConcurrentDictionary<nint, IAssetHandler> _cachedHander;
private readonly ConcurrentDictionary<Guid, WeakReference<Asset>> _loadedAssets;
private readonly Dictionary<Guid, HashSet<Guid>> _referencerGraph;
private readonly Dictionary<Guid, HashSet<Guid>> _dependencyCache;
private readonly ConcurrentDictionary<string, bool> _ignoreFileChanges;
private readonly SemaphoreSlim _cacheSlim;
private readonly Lock _pathLock;
public event EventHandler<IAssetRegistry, AssetChangedEventArgs>? OnAssetChanged;
public AssetRegistry(string rootDirectory)
{
if (!Directory.Exists(rootDirectory))
{
throw new DirectoryNotFoundException("The specified root directory does not exist.");
}
if (!Path.IsPathFullyQualified(rootDirectory))
{
throw new InvalidOperationException("The specified root directory must be an absolute path.");
}
_rootDirectory = rootDirectory;
_watcher = new FileSystemWatcher(rootDirectory)
{
IncludeSubdirectories = true,
EnableRaisingEvents = true,
};
_pathToGuid = new ConcurrentDictionary<string, Guid>(4, 512, new PathComparer());
_guidToPath = new ConcurrentDictionary<Guid, string>(4, 512);
_cachedHander = new ConcurrentDictionary<nint, IAssetHandler>(4, 16);
_loadedAssets = new ConcurrentDictionary<Guid, WeakReference<Asset>>(4, 512);
_referencerGraph = new Dictionary<Guid, HashSet<Guid>>();
_dependencyCache = new Dictionary<Guid, HashSet<Guid>>();
_ignoreFileChanges = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
_cacheSlim = new SemaphoreSlim(1, 1);
_pathLock = new Lock();
LoadExistingAssets();
_watcher.Created += OnFileSystemOp;
_watcher.Deleted += OnFileSystemOp;
_watcher.Changed += OnFileSystemOp;
_watcher.Renamed += OnFileSystemRenameOp;
}
// TODO: DB Cache
private unsafe void LoadExistingAssets()
{
Span<byte> guidBuffer = stackalloc byte[sizeof(Guid)];
foreach (var filePath in Directory.EnumerateFiles(_rootDirectory, $"*{ASSET_EXTENSION}", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(_rootDirectory, filePath);
try
{
var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
try
{
fs.Seek(4, SeekOrigin.Begin); // Skip format version
fs.ReadExactly(guidBuffer);
var guid = Unsafe.ReadUnaligned<Guid>(ref MemoryMarshal.GetReference(guidBuffer));
UpdatePathMapping(relativePath, guid);
}
finally
{
fs.Dispose();
}
}
catch (Exception
#if DEBUG
ex
#endif
)
{
#if DEBUG
System.Diagnostics.Debugger.BreakForUserUnhandledException(ex);
#endif
continue;
}
}
}
private void UpdateGraph(Guid assetId, IEnumerable<Guid> newDependencies)
{
// 1. Clean up old references (reverse)
if (_dependencyCache.TryGetValue(assetId, out var oldDeps))
{
foreach (var dep in oldDeps)
{
if (_referencerGraph.TryGetValue(dep, out var refs))
{
refs.Remove(assetId);
}
}
}
// 2. Set new forward dependencies
var newDepSet = new HashSet<Guid>(newDependencies);
_dependencyCache[assetId] = newDepSet;
// 3. Add new references (reverse)
foreach (var dep in newDepSet)
{
ref var referencers = ref CollectionsMarshal.GetValueRefOrAddDefault(_referencerGraph, dep, out var exists);
if (!exists || referencers is null)
{
referencers = new HashSet<Guid>();
}
referencers.Add(assetId);
}
}
private void UpdatePathMapping(string relativePath, Guid guid)
{
lock (_pathLock)
{
_pathToGuid[relativePath] = guid;
_guidToPath[guid] = relativePath;
}
}
private bool RemovePathMappingByPath(string relativePath)
{
lock (_pathLock)
{
if (_pathToGuid.Remove(relativePath, out var guid))
{
return _guidToPath.TryRemove(guid, out _);
}
}
return false;
}
private async void OnFileSystemOp(object sender, FileSystemEventArgs e)
{
if (_ignoreFileChanges.TryRemove(e.FullPath, out _))
{
return;
}
var relativePath = Path.GetRelativePath(_rootDirectory, e.FullPath);
var ext = Path.GetExtension(relativePath);
var changeType = AssetChangeType.None;
var fireEvent = false;
var isAsset = ext.Equals(ASSET_EXTENSION, StringComparison.Ordinal);
var isTemp = ext.Equals(TEMP_EXTENSION, StringComparison.Ordinal);
switch (e.ChangeType)
{
case WatcherChangeTypes.Created:
changeType = AssetChangeType.Created;
if (!isAsset && !isTemp)
{
var handler = GetAssetHandlerForExtension(ext);
if (handler is IImportableAssetHandler importableHandler)
{
var assetPath = string.Create(e.FullPath.Length - ext.Length + ASSET_EXTENSION.Length, e.FullPath, (destSpan, source) =>
{
source.AsSpan(0, source.Length - ext.Length).CopyTo(destSpan);
ASSET_EXTENSION.AsSpan().CopyTo(destSpan.Slice(source.Length - ext.Length));
});
var newGuid = Guid.NewGuid();
await using var sourceStream = new FileStream(e.FullPath, FileMode.Open, FileAccess.Read);
await using var targetStream = new FileStream(assetPath, FileMode.Create, FileAccess.Write);
await importableHandler.ImportAsync(sourceStream, targetStream, newGuid);
File.Delete(assetPath);
UpdatePathMapping(relativePath, newGuid);
fireEvent = true;
}
}
break;
case WatcherChangeTypes.Deleted:
changeType = AssetChangeType.Deleted;
if (isAsset)
{
fireEvent = RemovePathMappingByPath(relativePath);
}
break;
case WatcherChangeTypes.Changed:
changeType = AssetChangeType.Modified;
fireEvent = isAsset;
break;
case WatcherChangeTypes.All:
// Can this even happen?
break;
default:
break;
}
if (fireEvent)
{
OnAssetChanged?.Invoke(this, new AssetChangedEventArgs(relativePath, null, changeType));
}
}
private void OnFileSystemRenameOp(object sender, RenamedEventArgs e)
{
var ext = Path.GetExtension(e.FullPath);
if (!ext.Equals(ASSET_EXTENSION, StringComparison.Ordinal))
{
return;
}
var oldRelativePath = Path.GetRelativePath(_rootDirectory, e.OldFullPath);
var newRelativePath = Path.GetRelativePath(_rootDirectory, e.FullPath);
if (_pathToGuid.Remove(oldRelativePath, out var guid))
{
UpdatePathMapping(newRelativePath, guid);
OnAssetChanged?.Invoke(this, new AssetChangedEventArgs(newRelativePath, oldRelativePath, AssetChangeType.Renamed));
}
}
public string? GetAssetPath(Guid id)
{
lock (_pathLock)
{
if (_guidToPath.TryGetValue(id, out var path))
{
return path;
}
}
return null;
}
public Guid GetAssetGuid(string path)
{
lock (_pathLock)
{
if (_pathToGuid.TryGetValue(path, out var guid))
{
return guid;
}
}
return Guid.Empty;
}
private IAssetHandler GetAssetHandler(Type type)
{
var typeHandle = type.TypeHandle.Value;
if (_cachedHander.TryGetValue(typeHandle, out var handler))
{
return handler;
}
var obj = Activator.CreateInstance(type);
if (obj is not IAssetHandler newHandler)
{
throw new InvalidOperationException($"Type {type.FullName} is not an IAssetHandler.");
}
var attr = type.GetCustomAttribute<CustomAssetHandlerAttribute>(false);
if (attr is null || attr.AllowCaching)
{
_cachedHander[typeHandle] = newHandler;
}
return newHandler;
}
private IAssetHandler? GetAssetHandlerForExtension(string extension)
{
foreach (var handlerType in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => typeof(IAssetHandler).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract))
{
var attr = handlerType.GetCustomAttribute<CustomAssetHandlerAttribute>(false);
if (attr is not null && attr.SupportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return GetAssetHandler(handlerType);
}
}
return null;
}
private IAssetHandler? GetAssetHandlerForTypeId(Guid typeId)
{
foreach (var handlerType in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => typeof(IAssetHandler).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract))
{
var attr = handlerType.GetCustomAttribute<CustomAssetHandlerAttribute>(false);
if (attr is not null && new Guid(attr.ID) == typeId)
{
return GetAssetHandler(handlerType);
}
}
return null;
}
public async ValueTask<Result<Guid>> ImportAssetAsync(string sourceFilePath, string targetAssetPath, CancellationToken token = default)
{
if (!File.Exists(sourceFilePath))
{
return Result.Failure("Source file not found.");
}
var ext = Path.GetExtension(sourceFilePath);
var handler = GetAssetHandlerForExtension(ext);
if (handler is not IImportableAssetHandler importableHandler)
{
return Result.Failure("No importable asset handler found for the given file extension.");
}
var guid = Guid.NewGuid();
var fullTargetPath = Path.GetFullPath(targetAssetPath, _rootDirectory);
if (!await importableHandler.ImportAsync(sourceFilePath, fullTargetPath, guid, token: token))
{
return Result.Failure("Asset import failed.");
}
UpdatePathMapping(targetAssetPath, guid);
return guid;
}
public async ValueTask<Result> ReimportAssetAsync(Guid assetId, string sourceFilePath, CancellationToken token = default)
{
var assetPath = GetAssetPath(assetId);
if (string.IsNullOrEmpty(assetPath))
{
return Result.Failure("Asset not found in DB");
}
var fullAssetPath = Path.GetFullPath(assetPath, _rootDirectory);
// 2. Identify the Handler
// (You might want to store SourcePath in metadata later so you don't need to pass it here)
var ext = Path.GetExtension(sourceFilePath);
var handler = GetAssetHandlerForExtension(ext);
if (handler is not IImportableAssetHandler importableHandler)
{
return Result.Failure("No importable asset handler found for the given file extension.");
}
_ignoreFileChanges[fullAssetPath] = true;
await using var sourceStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.Read);
await using var targetStream = new FileStream(fullAssetPath, FileMode.Create, FileAccess.Write);
await importableHandler.ImportAsync(sourceStream, targetStream, assetId, token);
if (_loadedAssets.TryGetValue(assetId, out var weakRef) && weakRef.TryGetTarget(out var liveAsset))
{
await liveAsset.RefreshAsync(this, token);
}
return Result.Success();
}
public async ValueTask<Result<Asset>> LoadAssetAsync(Guid id, CancellationToken token = default)
{
// TODO: weakRef based locking instead of global lock for better concurrency.
// We should use GetOrAdd here.
if (_loadedAssets.TryGetValue(id, out var weakRef)
&& weakRef.TryGetTarget(out var existingAsset))
{
return existingAsset;
}
await _cacheSlim.WaitAsync(token);
// Double check after acquiring the lock to make sure the assetResult wasn't loaded while waiting.
if (_loadedAssets.TryGetValue(id, out weakRef)
&& weakRef.TryGetTarget(out existingAsset))
{
return existingAsset;
}
try
{
var path = GetAssetPath(id);
if (string.IsNullOrEmpty(path))
{
return null;
}
var assetPath = Path.GetFullPath(path, _rootDirectory);
await using var fs = new FileStream(assetPath, FileMode.Open, FileAccess.Read, FileShare.Read);
int sizeofGuid;
unsafe
{
sizeofGuid = sizeof(Guid);
}
Span<byte> typeIdBuffer = stackalloc byte[sizeofGuid];
fs.Seek(sizeof(int) + sizeofGuid, SeekOrigin.Begin);
fs.ReadExactly(typeIdBuffer);
var guid = Unsafe.ReadUnaligned<Guid>(ref MemoryMarshal.GetReference(typeIdBuffer));
var handler = GetAssetHandlerForTypeId(guid);
if (handler == null)
{
return null;
}
var assetResult = await handler.LoadAsync(fs, this, token);
if (assetResult.IsFailure)
{
return assetResult;
}
var asset = assetResult.Value;
_loadedAssets.AddOrUpdate(id, new WeakReference<Asset>(asset), (key, oldRef) =>
{
// If the early return fails (find existing assetResult), it means either the assetResult haven't been loaded before, or the previous reference has been collected.
// If the assetResult haven't been loaded before, we are in the addValue path, not here.
// If the previous reference has been collected, we can just replace it with the new one.
// Since we are using _cacheSlim to protect this section, we don't need check if the oldRef is still valid because only one thread can be here at a time.
oldRef.SetTarget(asset);
return oldRef;
});
return assetResult;
}
finally
{
_cacheSlim.Release();
}
}
public async ValueTask<Result> SaveAssetAsync(Asset asset, CancellationToken token = default)
{
var path = GetAssetPath(asset.ID);
if (path == null)
{
return Result.Failure("Asset not found.");
}
var handler = GetAssetHandlerForTypeId(asset.TypeID);
if (handler == null)
{
return Result.Failure("No asset handler found for the given asset type.");
}
var fullPath = Path.GetFullPath(path, _rootDirectory);
await using var fs = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
return await handler.SaveAsync(asset, fs, this, token);
}
public void Dispose()
{
_cacheSlim.Dispose();
_watcher.Dispose();
}
}

View File

@@ -0,0 +1,21 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
public class InspectorService : IInspectorService
{
private IInspectable? _selected;
public IInspectable? Selected => _selected;
public event EventHandler<InspectorSelectionChangedEventArgs>? OnSelectionChanged;
public void SetSelected(IInspectable? inspectable, object? source)
{
if (_selected != inspectable)
{
_selected = inspectable;
OnSelectionChanged?.Invoke(this, new InspectorSelectionChangedEventArgs(source, inspectable));
}
}
}

View File

@@ -0,0 +1,51 @@
using CommunityToolkit.WinUI.Behaviors;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Notifications;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Services;
public class NotificationService : INotificationService
{
private InfoBar? _infoBar;
private StackedNotificationsBehavior? _notificationQueue;
internal void SetReference(InfoBar infoBar, StackedNotificationsBehavior notificationQueue)
{
_infoBar = infoBar;
_notificationQueue = notificationQueue;
}
public void ShowNotification(string? message, MessageType type, int duration = 5, string? title = null)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}
var notification = new Notification
{
Message = message,
Severity = (InfoBarSeverity)type,
Duration = TimeSpan.FromSeconds(duration),
Title = title
};
ShowNotification(notification);
}
public void ShowNotification(Notification notification)
{
_notificationQueue?.Show(notification);
}
internal void ClearReference()
{
if (_infoBar != null)
{
_infoBar.IsOpen = false;
}
_infoBar = null;
_notificationQueue = null;
}
}

View File

@@ -0,0 +1,35 @@
using Ghost.Editor.Core.Contracts;
namespace Ghost.Editor.Core.Services;
internal class PreviewService : IPreviewService
{
public string GetIconPath(string path, bool isDirectory, IconSize size)
{
string iconPath;
if (isDirectory)
{
iconPath = "ms-appx:///Assets/EditorIcons/folder-{0}.png";
}
else
{
// TODO: Generate preview icons dynamically for known file types like images, meshes, materials, etc.
var ext = Path.GetExtension(path);
iconPath = ext switch
{
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".tiff" or ".svg" => "ms-appx:///Assets/EditorIcons/image-{0}.png",
_ => "ms-appx:///Assets/EditorIcons/document-{0}.png",
};
}
var sizeIndex = size switch
{
IconSize.Small => "0",
IconSize.Large => "1",
_ => "0"
};
iconPath = string.Format(iconPath, sizeIndex);
return iconPath;
}
}

View File

@@ -0,0 +1,75 @@
using CommunityToolkit.WinUI;
using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.CompilerServices;
namespace Ghost.Editor.Core.Services;
public class ProgressService : IProgressService
{
private Grid? _progressBarContainer;
private TextBlock? _progressMessage;
private ProgressBar? _progressBar;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsInitialized()
{
return _progressBarContainer != null && _progressMessage != null && _progressBar != null;
}
internal void SetReference(Grid progressBarContainer)
{
_progressBarContainer = progressBarContainer;
_progressMessage = _progressBarContainer.FindChild<TextBlock>();
_progressBar = _progressBarContainer.FindChild<ProgressBar>();
}
public void ShowProgress(string message, double progress = 0.0)
{
if (!IsInitialized())
{
return;
}
_progressBarContainer!.Visibility = Visibility.Visible;
_progressMessage!.Text = message;
_progressBar!.Value = progress;
}
public void ShowIndeterminateProgress(string message)
{
if (!IsInitialized())
{
return;
}
_progressBarContainer!.Visibility = Visibility.Visible;
_progressMessage!.Text = message;
_progressBar!.IsIndeterminate = true;
}
public void SetProgress(double progress)
{
_progressBar!.Value = progress;
}
public void HideProgress()
{
if (!IsInitialized())
{
return;
}
_progressBarContainer!.Visibility = Visibility.Collapsed;
_progressMessage!.Text = string.Empty;
_progressBar!.Value = 0.0;
}
internal void ClearReference()
{
_progressBarContainer = null;
_progressMessage = null;
_progressBar = null;
}
}

View File

@@ -0,0 +1,53 @@
using Ghost.Editor.Core.AssetHandler;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Utilities;
public static class AssetHandlerUtility
{
public static async ValueTask SerializeAssetAsync<TSetting>(Stream stream, Guid id, Guid typeID, int handlerVersion, ReadOnlyMemory<Guid> dependencies, IAssetSettings? settings, ReadOnlyMemory<byte> contents, CancellationToken token = default)
where TSetting : IAssetSettings
{
var header = new AssetMetadata(id, TextureAsset.s_typeGuid)
{
HandlerVersion = handlerVersion,
DependenciesOffset = AssetMetadata.SIZE,
DependencyCount = dependencies.Length,
};
var tempArray = ArrayPool<byte>.Shared.Rent(4096);
if (dependencies.Length > 0)
{
stream.Seek(header.DependenciesOffset, SeekOrigin.Begin);
for (var i = 0; i < dependencies.Length; i++)
{
Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(tempArray.AsSpan(0, 16)), dependencies.Span[i]);
await stream.WriteAsync(tempArray.AsMemory(0, 16), token);
}
}
header.SettingsOffset = stream.Position;
// TODO: We can use source generator to generate optimized serializer for settings.
// For now, we just use reflection for simplicity.
if (settings is not null)
{
var properties = typeof(TSetting).GetProperties();
if (properties.Length > 0)
{
using var bw = new BinaryWriter(stream);
for (var i = 0; (i < properties.Length); i++)
{
var property = properties[i];
var value = property.GetValue(settings);
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
namespace Ghost.Editor.Core.Utilities;
internal static class FileExtensions
{
public const string META_FILE_EXTENSION = ".gmeta";
public const string PROJECT_FILE_EXTENSION = ".gproj";
public const string TEMPLATE_FILE_EXTENSION = ".gtmpl";
public const string SCENE_FILE_EXTENSION = ".gscene";
public const string ASSET_FILE_EXTENSION = ".gasset";
public const string SHADER_FILE_EXTENSION = ".gshdr";
public const string MATERIAL_FILE_EXTENSION = ".gmat";
}

View File

@@ -0,0 +1,93 @@
using Ghost.Core.Attributes;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Utilities;
public static class TypeCache
{
private static TypeInfo[] s_types;
private static Dictionary<nint, List<MethodInfo>> s_attributeMethodCache;
static TypeCache()
{
s_types = LoadTypes();
s_attributeMethodCache = FindMethodWithAttribute();
}
private static TypeInfo[] LoadTypes()
{
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!);
}
}
return loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
}
private static Dictionary<nint, List<MethodInfo>> FindMethodWithAttribute()
{
var dict = new Dictionary<nint, List<MethodInfo>>();
foreach (var type in s_types)
{
foreach (var method in type.DeclaredMethods)
{
var attrs = method.GetCustomAttributes<DiscoverableAttributeBase>(false);
foreach (var attr in attrs)
{
var key = attr.GetType().TypeHandle.Value;
ref var methodList = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out var exist);
if (!exist)
{
methodList = new List<MethodInfo>();
}
methodList!.Add(method);
}
}
}
return dict;
}
internal static void Init()
{
// Intentionally left blank.
// This method exists to force the static constructor to run.
}
internal static void Reload()
{
s_types = LoadTypes();
s_attributeMethodCache = FindMethodWithAttribute();
}
public static IReadOnlyCollection<TypeInfo> GetTypes()
{
return s_types;
}
public static IReadOnlyCollection<MethodInfo>? GetMethodsWithAttribute<T>()
where T : DiscoverableAttributeBase
{
var key = typeof(T).TypeHandle.Value;
if (s_attributeMethodCache.TryGetValue(key, out var methods))
{
return methods;
}
return null;
}
}

View File

@@ -0,0 +1,70 @@
using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Models;
using Ghost.Engine;
using Microsoft.UI.Xaml;
using System.Reflection;
namespace Ghost.Editor;
internal static class ActivationHandler
{
public static LaunchArguments ParseArguments(ReadOnlySpan<char> args)
{
var arguments = new LaunchArguments();
var properties = typeof(LaunchArguments).GetProperties();
var split = args.Split(' ');
while (split.MoveNext())
{
var range = split.Current;
var arg = args[range.Start..range.End];
if (arg.Length > 2)
{
if (arg[0] == '-' && arg[1] == '-')
{
var argName = arg[2..];
foreach (var property in properties)
{
var propName = property.Name;
var attr = property.GetCustomAttributes<ArgumentNameAttribute>(false).FirstOrDefault();
if (attr != null)
{
propName = attr.Name;
}
if (argName.Equals(propName, StringComparison.OrdinalIgnoreCase))
{
if (split.MoveNext())
{
var valueRange = split.Current;
var value = args[valueRange.Start..valueRange.End];
var convertedValue = Convert.ChangeType(value.ToString(), property.PropertyType);
property.SetValue(arguments, convertedValue);
break;
}
}
}
}
}
}
return arguments;
}
public static async Task HandleAsync(LaunchArguments args)
{
await Task.Run(() =>
{
TypeCache.Init();
((EngineCore)App.GetService<IEngineContext>()).Init();
});
await ((Core.AssetHandle.AssetService)App.GetService<IAssetService>()).Init();
// TODO: Init other subsystems here.
// await Task.Delay(10000); // Wait 10 seconds to simulate work.
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application
x:Class="Ghost.Editor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:core="using:Ghost.Editor.Core.Controls"
xmlns:local="using:Ghost.Editor">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/Themes/Generic.xaml" />
<core:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,163 @@
using Ghost.Core;
using Ghost.Editor.Core;
using Ghost.Editor.Core.AssetHandle;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services;
using Ghost.Editor.View.Pages.EngineEditor;
using Ghost.Editor.View.Windows;
using Ghost.Editor.ViewModels.Controls;
using Ghost.Editor.ViewModels.Pages.EngineEditor;
using Ghost.Editor.ViewModels.Windows;
using Ghost.Engine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using System.Diagnostics;
using WinUIEx;
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) =>
{
services.AddSingleton<IEngineContext, EngineCore>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IProgressService, ProgressService>();
services.AddSingleton<IInspectorService, InspectorService>();
services.AddSingleton<IPreviewService, PreviewService>();
services.AddSingleton<IAssetService, AssetService>();
services.AddSingleton<EngineEditorViewModel>();
services.AddTransient<ProjectBrowserViewModel>();
#region Should be deleted
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>();
#endregion
})
.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;
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
base.OnLaunched(args);
var arguments = ActivationHandler.ParseArguments("--project-path F:/GhostProject/Test2 --project-name Test2"); // args.Arguments
if (!arguments.IsValid())
{
Exit();
return;
}
EditorApplication.Initialize(Host.Services, arguments.ProjectPath, arguments.ProjectName);
// NOTE: We must call DispatcherQueue.GetForCurrentThread() on the UI thread before any await.
EditorApplication.SetDispatcherQueue(DispatcherQueue.GetForCurrentThread());
var splashWindow = new SplashWindow();
splashWindow.Activate();
Window = splashWindow;
await Host.StartAsync();
await ActivationHandler.HandleAsync(arguments);
splashWindow.Hide();
var editorWindow = new EngineEditorWindow();
editorWindow.Activate();
Window = editorWindow;
splashWindow.Close();
}
private void OnClosed(object? sender, WindowEventArgs args)
{
try
{
Host.StopAsync().GetAwaiter().GetResult();
Host.Dispose();
EditorApplication.Shutdown();
}
catch (Exception ex)
{
Debugger.BreakForUserUnhandledException(ex);
}
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError(e.Exception);
#if DEBUG
Debugger.BreakForUserUnhandledException(e.Exception);
#endif
}
}

View File

@@ -0,0 +1,6 @@
using Ghost.Core.Attributes;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.UnitTest")]
[assembly: EngineAssembly]

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" width="48px" height="48px">
<defs>
<style>
.cls-1 {
fill: url(#Безымянный_градиент_199);
}
.cls-2 {
fill: #f7c13a;
}
</style>
<linearGradient id=езымянный_градиент_199" data-name="Безымянный градиент 199" x1="11.80415" y1="-22.237" x2="29.6085" y2="45.263" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#6d6d6d"/>
<stop offset="0.12552" stop-color="#626262"/>
<stop offset="0.987" stop-color="#464646"/>
<stop offset="0.998" stop-color="#454545"/>
</linearGradient>
</defs>
<rect class="cls-1" x="6" y="6" width="36" height="36" rx="2"/>
<path class="cls-2" d="M22,27v1.5a.5.5,0,0,1-.5.5H17a8,8,0,0,1-8-8V19.5a.5.5,0,0,1,.5-.5h4.00612a.501.501,0,0,1,.49758.49688C14.05815,23.13441,14.70582,26,15.5,26c.73344,0,1.33761-2.43083,1.46835-5.65979a.50624.50624,0,0,1,.74437-.42711A7.99072,7.99072,0,0,1,22,27Z"/>
<path class="cls-2" d="M39,19.5V21a8,8,0,0,1-8,8H26.5a.5.5,0,0,1-.5-.5V27a7.99072,7.99072,0,0,1,4.28728-7.0869.50624.50624,0,0,1,.74437.42711C31.16239,23.56917,31.76656,26,32.5,26c.79418,0,1.44185-2.86559,1.4963-6.50312A.501.501,0,0,1,34.49388,19H38.5A.5.5,0,0,1,39,19.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Some files were not shown because too many files have changed in this diff Show More