forked from Misaki/GhostEngine
Refactor project structure and enhance functionality
Added `InternalsVisibleTo` attribute for "Ghost.Editor" in `AssemblyInfo.cs`. Added a binary file `Empty.zip` to the project. Added a new `ProjectMetadata` class in `ProjectMetadata.cs`. Added new states and interfaces for managing application states in `EditorState.cs`, `LandingState.cs`, and `IAppState.cs`. Added a notification service in `INotificationService.cs` and `StackedNotificationService.cs`. Added new XAML files for UI components, including `InspectorView.xaml` and `InternalControls.xaml`. Changed the `ProjectInfo` class in `ProjectInfo.cs` to include a `MetadataPath` property instead of `Path` and `EngineVersion`. Changed the `TemplateInfo` class in `TemplateInfo.cs` to use a struct instead of a class for `TemplateData`. Changed the `ProjectService` class to use the new `ProjectRepository` for managing project data. Removed several using directives and the entire `ProjectRepository` class from `ProjectRepository.cs`, replacing it with a new implementation. Removed old methods and properties in `EntityManager` and `World` classes to improve entity management and component handling. Updated the `Ghost.Data.csproj` file to include the new `Empty.zip` file as a content item. Updated the `ProjectRepository` class to manage project data using SQLite. Updated various XAML files to include new styles and controls, improving the overall UI design. Updated the `CreateProjectViewModel` to include a notification service and handle project creation logic. Updated the test project to include references to the new `Ghost.Graphics` project and modified test cases to align with the new structure.
This commit is contained in:
3
Ghost.Data/AssemblyInfo.cs
Normal file
3
Ghost.Data/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
||||||
BIN
Ghost.Data/Assets/ProjectTemplates/Empty.zip
Normal file
BIN
Ghost.Data/Assets/ProjectTemplates/Empty.zip
Normal file
Binary file not shown.
@@ -1,101 +0,0 @@
|
|||||||
using Ghost.Data.Models;
|
|
||||||
using Ghost.Data.Resources;
|
|
||||||
using System.Data.SQLite;
|
|
||||||
|
|
||||||
namespace Ghost.Data.DataContext;
|
|
||||||
|
|
||||||
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, Path TEXT, EngineVersion TEXT, LastOpened DATETIME);";
|
|
||||||
public const string SELECT_PROJECT_STRING = "SELECT * FROM Projects";
|
|
||||||
public const string INSERT_PROJECT_STRING = "INSERT INTO Projects (Name, Path, EngineVersion, LastOpened) VALUES (@Name, @Path, @EngineVersion, @LastOpened);";
|
|
||||||
public const string REMOVE_PROJECT_STRING = "DELETE FROM Projects WHERE ID = @ID;";
|
|
||||||
public const string UPDATE_PROJECT_STRING = "UPDATE Projects SET Name = @Name, Path = @Path, EngineVersion = @EngineVersion, LastOpened = @LastOpened WHERE ID = @ID;";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetConnectionString() => string.Format(Command.CONNECTION_STRING, DataPath.APPLICATION_DATA_FOLDER);
|
|
||||||
|
|
||||||
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> LoadProjectsAsync()
|
|
||||||
{
|
|
||||||
using var connection = new SQLiteConnection(GetConnectionString());
|
|
||||||
await connection.OpenAsync();
|
|
||||||
|
|
||||||
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),
|
|
||||||
Path = reader.GetString(2),
|
|
||||||
EngineVersion = new Version(reader.GetString(3)),
|
|
||||||
LastOpened = reader.GetDateTime(4)
|
|
||||||
};
|
|
||||||
|
|
||||||
yield return project;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task AddProjectAsync(ProjectInfo project)
|
|
||||||
{
|
|
||||||
using var connection = new SQLiteConnection(GetConnectionString());
|
|
||||||
await connection.OpenAsync();
|
|
||||||
|
|
||||||
await EnsureTableCreatedAsync(connection);
|
|
||||||
|
|
||||||
using var command = connection.CreateCommand();
|
|
||||||
command.CommandText = Command.INSERT_PROJECT_STRING;
|
|
||||||
|
|
||||||
command.Parameters.AddWithValue("@Name", project.Name);
|
|
||||||
command.Parameters.AddWithValue("@Path", project.Path);
|
|
||||||
command.Parameters.AddWithValue("@EngineVersion", project.EngineVersion.ToString());
|
|
||||||
command.Parameters.AddWithValue("@LastOpened", project.LastOpened);
|
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task RemoveProjectAsync(ProjectInfo project)
|
|
||||||
{
|
|
||||||
using var connection = new SQLiteConnection(GetConnectionString());
|
|
||||||
await connection.OpenAsync();
|
|
||||||
|
|
||||||
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(GetConnectionString());
|
|
||||||
await connection.OpenAsync();
|
|
||||||
|
|
||||||
using var command = connection.CreateCommand();
|
|
||||||
command.CommandText = Command.UPDATE_PROJECT_STRING;
|
|
||||||
|
|
||||||
command.Parameters.AddWithValue("@Name", project.Name);
|
|
||||||
command.Parameters.AddWithValue("@Path", project.Path);
|
|
||||||
command.Parameters.AddWithValue("@EngineVersion", project.EngineVersion.ToString());
|
|
||||||
command.Parameters.AddWithValue("@LastOpened", project.LastOpened);
|
|
||||||
command.Parameters.AddWithValue("@ID", project.ID); // Ensure the ID parameter is added
|
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,4 +20,10 @@
|
|||||||
<PackageReference Include="System.Drawing.Common" Version="4.7.3" />
|
<PackageReference Include="System.Drawing.Common" Version="4.7.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\ProjectTemplates\Empty.zip">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Ghost.Data;
|
|||||||
|
|
||||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||||
[JsonSerializable(typeof(TemplateInfo))]
|
[JsonSerializable(typeof(TemplateInfo))]
|
||||||
|
[JsonSerializable(typeof(ProjectMetadata))]
|
||||||
internal partial class JsonContext : JsonSerializerContext
|
internal partial class JsonContext : JsonSerializerContext
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Ghost.Data.Models;
|
namespace Ghost.Data.Models;
|
||||||
|
|
||||||
public class ProjectInfo
|
internal class ProjectInfo
|
||||||
{
|
{
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public int ID
|
public int ID
|
||||||
@@ -15,17 +15,7 @@ public class ProjectInfo
|
|||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
public required string Path
|
public required string MetadataPath
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public required Version EngineVersion
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public required DateTime LastOpened
|
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|||||||
51
Ghost.Data/Models/ProjectMetadata.cs
Normal file
51
Ghost.Data/Models/ProjectMetadata.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
namespace Ghost.Data.Models;
|
||||||
|
|
||||||
|
public class ProjectMetadata
|
||||||
|
{
|
||||||
|
public const string PROJECT_EXTENSION = "ghostproj";
|
||||||
|
|
||||||
|
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
|
||||||
|
public ProjectMetadata()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ProjectMetadataInfo(string path, ProjectMetadata metadata)
|
||||||
|
{
|
||||||
|
public readonly string Path => path;
|
||||||
|
public readonly ProjectMetadata Metadata => metadata;
|
||||||
|
}
|
||||||
53
Ghost.Data/Models/Result.cs
Normal file
53
Ghost.Data/Models/Result.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace Ghost.Data.Models;
|
||||||
|
|
||||||
|
public readonly struct Result
|
||||||
|
{
|
||||||
|
public readonly bool success;
|
||||||
|
|
||||||
|
public readonly string? message;
|
||||||
|
|
||||||
|
public Result(bool success, string? message = null)
|
||||||
|
{
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result OK()
|
||||||
|
{
|
||||||
|
return new Result(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result Error(string? message)
|
||||||
|
{
|
||||||
|
return new Result(false, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => success ? "OK" : $"Error: {message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct Result<T>
|
||||||
|
{
|
||||||
|
public readonly bool success;
|
||||||
|
public readonly T? data;
|
||||||
|
|
||||||
|
public readonly string? message;
|
||||||
|
|
||||||
|
public Result(bool success, T? data, string? message = null)
|
||||||
|
{
|
||||||
|
this.success = success;
|
||||||
|
this.data = data;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<T> OK(T data)
|
||||||
|
{
|
||||||
|
return new Result<T>(true, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<T> Error(string? message)
|
||||||
|
{
|
||||||
|
return new Result<T>(false, default, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => success ? $"OK: {data}" : $"Error: {message}";
|
||||||
|
}
|
||||||
@@ -23,21 +23,21 @@ public class TemplateInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TemplateData(string templatePath, TemplateInfo info)
|
public struct TemplateData(string templatePath, TemplateInfo info)
|
||||||
{
|
{
|
||||||
private const string _ICON_NAME = "icon.png";
|
private const string _ICON_NAME = "icon.png";
|
||||||
private const string _PREVIEW_NAME = "preview.png";
|
private const string _PREVIEW_NAME = "preview.png";
|
||||||
|
|
||||||
public string directory = Path.GetDirectoryName(templatePath)!;
|
public string directory = Path.GetDirectoryName(templatePath)!;
|
||||||
|
|
||||||
public TemplateInfo Info => info;
|
public readonly TemplateInfo Info => info;
|
||||||
|
|
||||||
public Uri GetIconURI()
|
public readonly Uri GetIconURI()
|
||||||
{
|
{
|
||||||
return new Uri(Path.Combine(directory, _ICON_NAME));
|
return new Uri(Path.Combine(directory, _ICON_NAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri GetPreviewURI()
|
public readonly Uri GetPreviewURI()
|
||||||
{
|
{
|
||||||
return new Uri(Path.Combine(directory, _PREVIEW_NAME));
|
return new Uri(Path.Combine(directory, _PREVIEW_NAME));
|
||||||
}
|
}
|
||||||
|
|||||||
99
Ghost.Data/Repository/ProjectRepository.cs
Normal file
99
Ghost.Data/Repository/ProjectRepository.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
using Ghost.Data.Models;
|
||||||
|
using System.Data;
|
||||||
|
using System.Data.SQLite;
|
||||||
|
|
||||||
|
namespace Ghost.Data.Repository;
|
||||||
|
|
||||||
|
internal class ProjectRepository : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SQLiteConnection _connection;
|
||||||
|
|
||||||
|
public ProjectRepository(string sourceDirectory)
|
||||||
|
{
|
||||||
|
_connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, sourceDirectory));
|
||||||
|
_connection.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 async Task EnsureTableCreatedAsync()
|
||||||
|
{
|
||||||
|
using var createCommand = _connection.CreateCommand();
|
||||||
|
createCommand.CommandText = Command.CREATE_PROJECT_TABLE_STRING;
|
||||||
|
await createCommand.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<ProjectInfo> LoadProjectsAsync()
|
||||||
|
{
|
||||||
|
await EnsureTableCreatedAsync();
|
||||||
|
|
||||||
|
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 async Task AddProjectAsync(ProjectInfo project)
|
||||||
|
{
|
||||||
|
await EnsureTableCreatedAsync();
|
||||||
|
|
||||||
|
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 async Task RemoveProjectAsync(ProjectInfo project)
|
||||||
|
{
|
||||||
|
using var command = _connection.CreateCommand();
|
||||||
|
command.CommandText = Command.REMOVE_PROJECT_STRING;
|
||||||
|
|
||||||
|
command.Parameters.AddWithValue("@ID", project.ID);
|
||||||
|
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateProjectAsync(ProjectInfo project)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
_connection.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,5 +4,5 @@ public static class AssetsPath
|
|||||||
{
|
{
|
||||||
public const string ASSETS_FOLDER = "Assets";
|
public const string ASSETS_FOLDER = "Assets";
|
||||||
|
|
||||||
public readonly static string AppIconPath = Path.Combine(AppContext.BaseDirectory, $"{ASSETS_FOLDER}/Icon-256.ico");
|
public readonly static string s_appIconPath = Path.Combine(AppContext.BaseDirectory, $"{ASSETS_FOLDER}/Icon-256.ico");
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,6 @@ public class DataPath
|
|||||||
{
|
{
|
||||||
public const string ENGINE_DATA_FOLDER_NAME = "GhostEngine";
|
public const string ENGINE_DATA_FOLDER_NAME = "GhostEngine";
|
||||||
|
|
||||||
public readonly static string APPLICATION_DATA_FOLDER = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME);
|
public readonly static string s_applicationDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME);
|
||||||
public readonly static string PROJECT_TEMPLATES_FOLDER = Path.Combine(APPLICATION_DATA_FOLDER, "ProjectTemplates");
|
public readonly static string s_projectTemplateFolder = Path.Combine(s_applicationDataFolder, "ProjectTemplates");
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,38 @@
|
|||||||
using Ghost.Data.DataContext;
|
using Ghost.Data.Models;
|
||||||
using Ghost.Data.Models;
|
using Ghost.Data.Repository;
|
||||||
using Ghost.Data.Resources;
|
using Ghost.Data.Resources;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Ghost.Data.Services;
|
namespace Ghost.Data.Services;
|
||||||
|
|
||||||
public class ProjectService
|
internal partial class ProjectService
|
||||||
{
|
{
|
||||||
private const string _ASSETS_FOLDER = "Assets";
|
private const string _ASSETS_FOLDER = "Assets";
|
||||||
|
private const string _CONFIG_FOLDER = "ProjectConfig";
|
||||||
private const string _TEMPLATE_CONTENT_FILE = "content.zip";
|
private const string _TEMPLATE_CONTENT_FILE = "content.zip";
|
||||||
|
|
||||||
public async IAsyncEnumerable<(string path, TemplateInfo info)> GetProjectTemplatesAsync()
|
public static void EnsureDefaultTemplate()
|
||||||
{
|
{
|
||||||
var templatesFolder = DataPath.PROJECT_TEMPLATES_FOLDER;
|
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))
|
if (!Directory.Exists(templatesFolder))
|
||||||
{
|
{
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var templates = Directory.GetFiles(DataPath.PROJECT_TEMPLATES_FOLDER, "template.json", SearchOption.AllDirectories);
|
var templates = Directory.GetFiles(DataPath.s_projectTemplateFolder, "template.json", SearchOption.AllDirectories);
|
||||||
foreach (var templatePath in templates)
|
foreach (var templatePath in templates)
|
||||||
{
|
{
|
||||||
var fileStream = File.OpenRead(templatePath);
|
var fileStream = File.OpenRead(templatePath);
|
||||||
@@ -33,68 +46,152 @@ public class ProjectService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SetupAssetsFolder(string projectPath, string templatePath)
|
public static async Task CreateMetadataFileAsync(string path, ProjectMetadata metadata)
|
||||||
{
|
{
|
||||||
return Task.Run(() =>
|
await using var fileStream = File.Create(path);
|
||||||
{
|
await JsonSerializer.SerializeAsync(fileStream, metadata, JsonContext.Default.ProjectMetadata);
|
||||||
var templateContentPath = Path.Combine(templatePath, _TEMPLATE_CONTENT_FILE);
|
|
||||||
var projectAssetsPath = Path.Combine(projectPath, _ASSETS_FOLDER);
|
|
||||||
|
|
||||||
Directory.CreateDirectory(projectAssetsPath);
|
|
||||||
|
|
||||||
if (!File.Exists(templateContentPath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ZipFile.ExtractToDirectory(templateContentPath, projectAssetsPath);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAsyncEnumerable<ProjectInfo> LoadAllProjectAsync()
|
public static async Task<ProjectMetadata?> LoadMetadataAsync(string ghostprojPath)
|
||||||
{
|
{
|
||||||
return ProjectRepository.LoadProjectsAsync();
|
if (!File.Exists(ghostprojPath))
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> CreateProjectAsync(string projectName, string projectDirectory, string templatePath)
|
|
||||||
{
|
|
||||||
var projectPath = Path.Combine(projectDirectory, projectName);
|
|
||||||
if (!Directory.Exists(projectPath))
|
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(projectPath);
|
throw new FileNotFoundException("Project metadata file not found.", ghostprojPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
await SetupAssetsFolder(projectPath, templatePath);
|
await using var fileStream = File.OpenRead(ghostprojPath);
|
||||||
|
return await JsonSerializer.DeserializeAsync<ProjectMetadata>(fileStream, JsonContext.Default.ProjectMetadata);
|
||||||
return projectPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<Result<ProjectMetadataInfo>> ValidateProjectDirectoryAsync(string? projectDirectory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(projectDirectory) || !Directory.Exists(projectDirectory))
|
||||||
|
{
|
||||||
|
return Result<ProjectMetadataInfo>.Error("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>.Error("Project folder structure is invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataPath = Directory.GetFiles(projectDirectory, $"*.{ProjectMetadata.PROJECT_EXTENSION}", SearchOption.TopDirectoryOnly).FirstOrDefault();
|
||||||
|
if (string.IsNullOrWhiteSpace(metadataPath) || !File.Exists(metadataPath))
|
||||||
|
{
|
||||||
|
return Result<ProjectMetadataInfo>.Error("Project metadata file not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = await LoadMetadataAsync(metadataPath);
|
||||||
|
if (metadata == null)
|
||||||
|
{
|
||||||
|
return Result<ProjectMetadataInfo>.Error("Project metadata file is corrupted or invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result<ProjectMetadataInfo>.OK(new(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 : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ProjectRepository _repository = new(DataPath.s_applicationDataFolder);
|
||||||
|
|
||||||
public Task AddProjectAsync(ProjectInfo project)
|
public Task AddProjectAsync(ProjectInfo project)
|
||||||
{
|
{
|
||||||
return ProjectRepository.AddProjectAsync(project);
|
return _repository.AddProjectAsync(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ProjectInfo> AddProjectAsync(string name, string path, Version version)
|
public async Task<ProjectInfo> AddProjectAsync(string name, string path)
|
||||||
{
|
{
|
||||||
var project = new ProjectInfo
|
var project = new ProjectInfo
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Path = path,
|
MetadataPath = path,
|
||||||
EngineVersion = version,
|
|
||||||
LastOpened = DateTime.Now
|
|
||||||
};
|
};
|
||||||
await ProjectRepository.AddProjectAsync(project);
|
await _repository.AddProjectAsync(project);
|
||||||
|
|
||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task RemoveProjectAsync(ProjectInfo project)
|
public Task RemoveProjectAsync(ProjectInfo project)
|
||||||
{
|
{
|
||||||
return ProjectRepository.RemoveProjectAsync(project);
|
return _repository.RemoveProjectAsync(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task UpdateProjectAsync(ProjectInfo project)
|
public Task UpdateProjectAsync(ProjectInfo project)
|
||||||
{
|
{
|
||||||
return ProjectRepository.UpdateProjectAsync(project);
|
return _repository.UpdateProjectAsync(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAsyncEnumerable<ProjectInfo> LoadAllProjectAsync()
|
||||||
|
{
|
||||||
|
return _repository.LoadProjectsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<ProjectInfo>> 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 new(false, null, "Directory is not empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = new ProjectMetadata(projectName, engineVersion);
|
||||||
|
var metadataPath = Path.Combine(projectPath, $"{projectName}.{ProjectMetadata.PROJECT_EXTENSION}");
|
||||||
|
await CreateMetadataFileAsync(metadataPath, metadata);
|
||||||
|
await SetupRequestFolderAsync(projectPath, templatePath);
|
||||||
|
|
||||||
|
var info = await AddProjectAsync(projectName, metadataPath);
|
||||||
|
return new(true, info);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Result<ProjectInfo>.Error($"Failed to create project: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<ProjectMetadataInfo>> AddProjectFromDirectoryAsync(string projectDirectory)
|
||||||
|
{
|
||||||
|
var result = await ValidateProjectDirectoryAsync(projectDirectory);
|
||||||
|
if (result.success)
|
||||||
|
{
|
||||||
|
await AddProjectAsync(result.data.Metadata.Name, result.data.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_repository.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Ghost.Data.Resources;
|
using Ghost.Data.Resources;
|
||||||
|
using Ghost.Data.Services;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
@@ -8,19 +9,20 @@ internal static class ActivationHandler
|
|||||||
{
|
{
|
||||||
private static void FolderInitialization()
|
private static void FolderInitialization()
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(DataPath.APPLICATION_DATA_FOLDER))
|
if (!Directory.Exists(DataPath.s_applicationDataFolder))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(DataPath.APPLICATION_DATA_FOLDER);
|
Directory.CreateDirectory(DataPath.s_applicationDataFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Directory.Exists(DataPath.PROJECT_TEMPLATES_FOLDER))
|
if (!Directory.Exists(DataPath.s_projectTemplateFolder))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(DataPath.PROJECT_TEMPLATES_FOLDER);
|
Directory.CreateDirectory(DataPath.s_projectTemplateFolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Handle(LaunchActivatedEventArgs args)
|
public static void Handle(LaunchActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
FolderInitialization();
|
FolderInitialization();
|
||||||
|
ProjectService.EnsureDefaultTemplate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||||
<XamlControlsResources Source="/Controls/EditorControls.xaml" />
|
<XamlControlsResources Source="/Controls/EditorControls.xaml" />
|
||||||
|
<ResourceDictionary Source="/Themes/Dark.xaml" />
|
||||||
|
<ResourceDictionary Source="/Themes/Light.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
<!-- Other app resources here -->
|
<!-- Other app resources here -->
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Ghost.Data.Services;
|
using Ghost.Editor.AppStates;
|
||||||
using Ghost.Editor.Helpers;
|
using Ghost.Editor.Helpers;
|
||||||
using Ghost.Editor.View.Windows;
|
using Ghost.Editor.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
@@ -18,6 +18,18 @@ namespace Ghost.Editor
|
|||||||
{
|
{
|
||||||
private Window? _window;
|
private Window? _window;
|
||||||
|
|
||||||
|
internal static Window? Window
|
||||||
|
{
|
||||||
|
get => (Current as App)?._window;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (Current is App app)
|
||||||
|
{
|
||||||
|
app._window = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal IHost Host
|
internal IHost Host
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
@@ -36,27 +48,29 @@ namespace Ghost.Editor
|
|||||||
UseContentRoot(AppContext.BaseDirectory).
|
UseContentRoot(AppContext.BaseDirectory).
|
||||||
ConfigureServices((context, services) =>
|
ConfigureServices((context, services) =>
|
||||||
{
|
{
|
||||||
services.AddSingleton<ProjectService>();
|
services.AddSingleton(sp =>
|
||||||
|
{
|
||||||
|
return new AppStateService(
|
||||||
|
new LandingState(),
|
||||||
|
new EditorState());
|
||||||
|
});
|
||||||
|
|
||||||
HostHelper.SetupPageService(context, services);
|
HostHelper.AddLandingScope(context, services);
|
||||||
|
HostHelper.AddEngineScope(context, services);
|
||||||
|
|
||||||
|
services.AddSingleton<StackedNotificationService>();
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
UnhandledException += App_UnhandledException;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Window? GetWindow()
|
internal static IServiceScope CreateScope()
|
||||||
{
|
{
|
||||||
return (Current as App)?._window;
|
return (Current as App)!.Host.Services.CreateScope();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void SetWindow(Window window)
|
public static T GetService<T>() where T : class
|
||||||
{
|
|
||||||
if (Current is App app)
|
|
||||||
{
|
|
||||||
app._window = window;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static T GetService<T>() where T : class
|
|
||||||
{
|
{
|
||||||
if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
|
if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
|
||||||
{
|
{
|
||||||
@@ -70,7 +84,7 @@ namespace Ghost.Editor
|
|||||||
/// Invoked when the application is launched.
|
/// Invoked when the application is launched.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="args">Details about the launch request and process.</param>
|
/// <param name="args">Details about the launch request and process.</param>
|
||||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
protected override async void OnLaunched(LaunchActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
base.OnLaunched(args);
|
base.OnLaunched(args);
|
||||||
|
|
||||||
@@ -78,8 +92,13 @@ namespace Ghost.Editor
|
|||||||
|
|
||||||
Host.Start();
|
Host.Start();
|
||||||
|
|
||||||
_window = GetService<LandingWindow>();
|
await GetService<AppStateService>().TransitionToAsync(StateKey.Landing);
|
||||||
_window.Activate();
|
}
|
||||||
|
|
||||||
|
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
// TODO: Log and handle exceptions as appropriate.
|
||||||
|
// https://docs.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application.unhandledexception.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
54
Ghost.Editor/AppStates/EditorState.cs
Normal file
54
Ghost.Editor/AppStates/EditorState.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using Ghost.Data.Models;
|
||||||
|
using Ghost.Editor.Contracts;
|
||||||
|
using Ghost.Editor.View.Windows;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.AppStates;
|
||||||
|
|
||||||
|
internal class EditorState : IAppState<StateKey>
|
||||||
|
{
|
||||||
|
private EngineEditorWindow? _window;
|
||||||
|
|
||||||
|
public StateKey StateKy => StateKey.EngineEditor;
|
||||||
|
|
||||||
|
public Task OnExitingAsync()
|
||||||
|
{
|
||||||
|
if (App.Window == _window)
|
||||||
|
{
|
||||||
|
App.Window = null;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnEnteringAsync(object? parameter)
|
||||||
|
{
|
||||||
|
if (parameter is not ProjectMetadata metadata)
|
||||||
|
{
|
||||||
|
throw new System.ArgumentException("Parameter must be of type ProjectMetadata.", nameof(parameter));
|
||||||
|
}
|
||||||
|
|
||||||
|
_window = App.GetService<EngineEditorWindow>();
|
||||||
|
_window.ViewModel.CurrentProject = metadata;
|
||||||
|
_window.Activate();
|
||||||
|
|
||||||
|
App.Window = _window;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnExitedAsync()
|
||||||
|
{
|
||||||
|
if (App.Window == _window)
|
||||||
|
{
|
||||||
|
App.Window = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_window?.Close();
|
||||||
|
_window = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnEnteredAsync(object? parameter)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Ghost.Editor/AppStates/LandingState.cs
Normal file
47
Ghost.Editor/AppStates/LandingState.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Ghost.Editor.Contracts;
|
||||||
|
using Ghost.Editor.View.Windows;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.AppStates;
|
||||||
|
|
||||||
|
internal class LandingState : IAppState<StateKey>
|
||||||
|
{
|
||||||
|
private LandingWindow? _window;
|
||||||
|
|
||||||
|
public StateKey StateKy => StateKey.Landing;
|
||||||
|
|
||||||
|
public Task OnExitingAsync()
|
||||||
|
{
|
||||||
|
if (App.Window == _window)
|
||||||
|
{
|
||||||
|
App.Window = null;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnEnteringAsync(object? parameter)
|
||||||
|
{
|
||||||
|
_window = App.GetService<LandingWindow>();
|
||||||
|
App.Window = _window;
|
||||||
|
|
||||||
|
_window.Activate();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnExitedAsync()
|
||||||
|
{
|
||||||
|
if (App.Window == _window)
|
||||||
|
{
|
||||||
|
App.Window = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_window?.Close();
|
||||||
|
_window = null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnEnteredAsync(object? parameter)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Ghost.Editor/AppStates/StateKey.cs
Normal file
8
Ghost.Editor/AppStates/StateKey.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Ghost.Editor.AppStates;
|
||||||
|
|
||||||
|
internal enum StateKey
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Landing,
|
||||||
|
EngineEditor,
|
||||||
|
}
|
||||||
33
Ghost.Editor/Contracts/IAppState.cs
Normal file
33
Ghost.Editor/Contracts/IAppState.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Contracts;
|
||||||
|
|
||||||
|
internal interface IAppState<Key>
|
||||||
|
{
|
||||||
|
public Key StateKy
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when exiting the state.
|
||||||
|
/// </summary>
|
||||||
|
public Task OnExitingAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when entering the state, right after OnEnteringAsync.
|
||||||
|
/// <paramref name="parameter">can be used to pass data into the state, such as a project to load.</summary>
|
||||||
|
/// </summary>
|
||||||
|
public Task OnEnteringAsync(object? parameter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when exiting the state, specifically for pose transitions.
|
||||||
|
/// </summary>
|
||||||
|
public Task OnExitedAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when entered the state, specifically after the state has been fully initialized and is ready for interaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameter">can be used to pass data into the state, such as a project to load.</param>
|
||||||
|
public Task OnEnteredAsync(object? parameter);
|
||||||
|
}
|
||||||
14
Ghost.Editor/Contracts/INotificationService.cs
Normal file
14
Ghost.Editor/Contracts/INotificationService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Contracts;
|
||||||
|
|
||||||
|
internal interface INotificationService
|
||||||
|
{
|
||||||
|
public void ShowNotification(string? message, InfoBarSeverity severity, int duration = 5, string? title = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface INotificationService<T> : INotificationService
|
||||||
|
{
|
||||||
|
public void Initialize(T notificationQueue);
|
||||||
|
public void ClearQueueReference();
|
||||||
|
}
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary Source="/Controls/BasicInput/PropertyField.xaml" />
|
<ResourceDictionary Source="/Controls/BasicInput/PropertyField.xaml" />
|
||||||
|
<ResourceDictionary Source="/Controls/Internal/InternalControls.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
24
Ghost.Editor/Controls/Internal/InspectorView.cs
Normal file
24
Ghost.Editor/Controls/Internal/InspectorView.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Controls.Internal;
|
||||||
|
|
||||||
|
internal sealed partial class InspectorView : ContentControl
|
||||||
|
{
|
||||||
|
public UIElement? Header
|
||||||
|
{
|
||||||
|
get => (UIElement)GetValue(HeaderProperty);
|
||||||
|
set => SetValue(HeaderProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
|
||||||
|
nameof(Header),
|
||||||
|
typeof(UIElement),
|
||||||
|
typeof(InspectorView),
|
||||||
|
new PropertyMetadata(null));
|
||||||
|
|
||||||
|
public InspectorView()
|
||||||
|
{
|
||||||
|
DefaultStyleKey = typeof(InspectorView);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Ghost.Editor/Controls/Internal/InspectorView.xaml
Normal file
41
Ghost.Editor/Controls/Internal/InspectorView.xaml
Normal 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.Controls.Internal">
|
||||||
|
<Style TargetType="local:InspectorView">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="local:InspectorView">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="50" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<Grid Grid.Row="0">
|
||||||
|
<ContentPresenter
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{TemplateBinding Header}"
|
||||||
|
ContentTemplate="{TemplateBinding HeaderTemplate}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||||
|
<ContentPresenter
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
ContentTransitions="{TemplateBinding ContentTransitions}" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
4
Ghost.Editor/Controls/Internal/InternalControls.xaml
Normal file
4
Ghost.Editor/Controls/Internal/InternalControls.xaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?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>
|
||||||
@@ -42,6 +42,12 @@
|
|||||||
<None Remove="Assets\Icon.targetsize-48_altform-unplated.png" />
|
<None Remove="Assets\Icon.targetsize-48_altform-unplated.png" />
|
||||||
<None Remove="Controls\BasicInput\PropertyField.xaml" />
|
<None Remove="Controls\BasicInput\PropertyField.xaml" />
|
||||||
<None Remove="Controls\EditorControls.xaml" />
|
<None Remove="Controls\EditorControls.xaml" />
|
||||||
|
<None Remove="Controls\Internal\InspectorView.xaml" />
|
||||||
|
<None Remove="Controls\Internal\InternalControls.xaml" />
|
||||||
|
<None Remove="Themes\Dark.xaml" />
|
||||||
|
<None Remove="Themes\Light.xaml" />
|
||||||
|
<None Remove="View\Pages\EngineEditor\ConsolePage.xaml" />
|
||||||
|
<None Remove="View\Pages\EngineEditor\ProjectPage.xaml" />
|
||||||
<None Remove="View\Pages\Landing\CreateProjectPage.xaml" />
|
<None Remove="View\Pages\Landing\CreateProjectPage.xaml" />
|
||||||
<None Remove="View\Pages\Landing\OpenProjectPage.xaml" />
|
<None Remove="View\Pages\Landing\OpenProjectPage.xaml" />
|
||||||
<None Remove="View\Windows\EngineEditorWindow.xaml" />
|
<None Remove="View\Windows\EngineEditorWindow.xaml" />
|
||||||
@@ -69,12 +75,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.1.240916" />
|
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.250129-preview2" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.250402" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250310001" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||||
<PackageReference Include="WinUIEx" Version="2.5.1" />
|
<PackageReference Include="WinUIEx" Version="2.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -98,10 +105,43 @@
|
|||||||
</Page>
|
</Page>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Controls\Internal\" />
|
<Folder Include="Helpers\Converters\" />
|
||||||
<Folder Include="Models\" />
|
|
||||||
<Folder Include="Resources\" />
|
<Folder Include="Resources\" />
|
||||||
<Folder Include="Services\" />
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Misaki.HighPerformance.Unsafe">
|
||||||
|
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="View\Pages\EngineEditor\ProjectPage.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="View\Pages\EngineEditor\ConsolePage.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Themes\Light.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Themes\Dark.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Controls\Internal\InternalControls.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Controls\Internal\InspectorView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Update="View\Windows\EngineEditorWindow.xaml">
|
<Page Update="View\Windows\EngineEditorWindow.xaml">
|
||||||
|
|||||||
30
Ghost.Editor/Helpers/ComponentTypeCache.cs
Normal file
30
Ghost.Editor/Helpers/ComponentTypeCache.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Ghost.Entities;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Helpers;
|
||||||
|
|
||||||
|
public static class ComponentTypeCache
|
||||||
|
{
|
||||||
|
private static readonly Type?[][] _componentTypes;
|
||||||
|
|
||||||
|
static ComponentTypeCache()
|
||||||
|
{
|
||||||
|
_componentTypes = new Type[World.WorldCount][];
|
||||||
|
for (var i = 0; i < World.WorldCount; i++)
|
||||||
|
{
|
||||||
|
var world = World.GetWorld(i);
|
||||||
|
var typeHandles = world.ComponentStorage.ComponentPools.Keys;
|
||||||
|
_componentTypes[i] = typeHandles.Select(handle => Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(handle))).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Type?[] GetComponentTypes(int worldIndex)
|
||||||
|
{
|
||||||
|
if (worldIndex < 0 || worldIndex >= _componentTypes.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(worldIndex), "Invalid world index.");
|
||||||
|
}
|
||||||
|
return _componentTypes[worldIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Helpers.Converters;
|
||||||
|
|
||||||
|
public partial class GetDirectoryNameConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
return value is string path ? System.IO.Path.GetDirectoryName(path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Ghost.Editor.View.Pages.Landing;
|
using Ghost.Data.Services;
|
||||||
|
using Ghost.Editor.View.Pages.Landing;
|
||||||
using Ghost.Editor.View.Windows;
|
using Ghost.Editor.View.Windows;
|
||||||
using Ghost.Editor.ViewModel.Pages.Landing;
|
using Ghost.Editor.ViewModel.Pages.Landing;
|
||||||
using Ghost.Editor.ViewModel.Windows;
|
using Ghost.Editor.ViewModel.Windows;
|
||||||
@@ -9,7 +10,7 @@ namespace Ghost.Editor.Helpers;
|
|||||||
|
|
||||||
internal static partial class HostHelper
|
internal static partial class HostHelper
|
||||||
{
|
{
|
||||||
public static void SetupPageService(HostBuilderContext context, IServiceCollection services)
|
public static void AddLandingScope(HostBuilderContext context, IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<LandingWindow>();
|
services.AddSingleton<LandingWindow>();
|
||||||
|
|
||||||
@@ -18,6 +19,11 @@ internal static partial class HostHelper
|
|||||||
|
|
||||||
services.AddTransient<OpenProjectPage>();
|
services.AddTransient<OpenProjectPage>();
|
||||||
|
|
||||||
|
services.AddTransient<ProjectService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddEngineScope(HostBuilderContext context, IServiceCollection services)
|
||||||
|
{
|
||||||
services.AddSingleton<EngineEditorWindow>();
|
services.AddSingleton<EngineEditorWindow>();
|
||||||
services.AddSingleton<EngineEditorViewModel>();
|
services.AddSingleton<EngineEditorViewModel>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
using Windows.Storage.Pickers;
|
using Windows.Storage.Pickers;
|
||||||
@@ -11,7 +12,7 @@ public static class SystemUtilities
|
|||||||
public static async Task<StorageFolder?> OpenFolderPickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "")
|
public static async Task<StorageFolder?> OpenFolderPickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "")
|
||||||
{
|
{
|
||||||
var openPicker = new FolderPicker();
|
var openPicker = new FolderPicker();
|
||||||
var hWnd = WindowNative.GetWindowHandle(App.GetWindow());
|
var hWnd = WindowNative.GetWindowHandle(App.Window);
|
||||||
InitializeWithWindow.Initialize(openPicker, hWnd);
|
InitializeWithWindow.Initialize(openPicker, hWnd);
|
||||||
|
|
||||||
openPicker.SuggestedStartLocation = startLocation;
|
openPicker.SuggestedStartLocation = startLocation;
|
||||||
@@ -21,4 +22,21 @@ public static class SystemUtilities
|
|||||||
var folder = await openPicker.PickSingleFolderAsync();
|
var folder = await openPicker.PickSingleFolderAsync();
|
||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<StorageFile?> OpenFilePickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "", params IEnumerable<string> filter)
|
||||||
|
{
|
||||||
|
var openPicker = new FileOpenPicker();
|
||||||
|
var hWnd = WindowNative.GetWindowHandle(App.Window);
|
||||||
|
InitializeWithWindow.Initialize(openPicker, hWnd);
|
||||||
|
|
||||||
|
openPicker.SuggestedStartLocation = startLocation;
|
||||||
|
openPicker.SettingsIdentifier = settingsIdentifier;
|
||||||
|
foreach (var fileType in filter)
|
||||||
|
{
|
||||||
|
openPicker.FileTypeFilter.Add(fileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = await openPicker.PickSingleFileAsync();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
269
Ghost.Editor/Models/GameObject.cs
Normal file
269
Ghost.Editor/Models/GameObject.cs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Ghost.Entities;
|
||||||
|
using Ghost.Entities.Helpers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Models;
|
||||||
|
|
||||||
|
public partial class GameObject : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsActive
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsActiveHierarchy
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entity Entity
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Scene Scene
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameObject? Parent
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
internal set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial ObservableCollection<IComponentData>? Components
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial IEnumerable<ScriptComponent>? ScriptComponents
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial ObservableCollection<GameObject>? Children
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameObject(Scene scene, string name)
|
||||||
|
{
|
||||||
|
Entity = scene.World.EntityManager.CreateEntity();
|
||||||
|
Scene = scene;
|
||||||
|
Name = name;
|
||||||
|
IsActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsActiveChanged(bool value)
|
||||||
|
{
|
||||||
|
IsActiveHierarchy = value && (Parent?.IsActiveHierarchy ?? true);
|
||||||
|
HandleActiveStateChanged();
|
||||||
|
|
||||||
|
if (Children != null)
|
||||||
|
{
|
||||||
|
foreach (var child in Children)
|
||||||
|
{
|
||||||
|
child.IsActiveHierarchy = value && IsActiveHierarchy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsActiveHierarchyChanged(bool value)
|
||||||
|
{
|
||||||
|
HandleActiveStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleActiveStateChanged()
|
||||||
|
{
|
||||||
|
if (IsActive && IsActiveHierarchy)
|
||||||
|
{
|
||||||
|
OnEnable();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OnEnable()
|
||||||
|
{
|
||||||
|
if (ScriptComponents != null)
|
||||||
|
{
|
||||||
|
foreach (var script in ScriptComponents)
|
||||||
|
{
|
||||||
|
if (!script.Enable)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
script.OnEnable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OnDisable()
|
||||||
|
{
|
||||||
|
if (ScriptComponents != null)
|
||||||
|
{
|
||||||
|
foreach (var script in ScriptComponents)
|
||||||
|
{
|
||||||
|
if (!script.Enable)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
script.OnDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddChild(GameObject child)
|
||||||
|
{
|
||||||
|
if (child.Scene != Scene)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Child GameObject must belong to the same Scene.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Children ??= new();
|
||||||
|
Children.Add(child);
|
||||||
|
child.Parent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveChild(GameObject child)
|
||||||
|
{
|
||||||
|
if (Children is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Children.Remove(child))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Parent = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Destroy()
|
||||||
|
{
|
||||||
|
if (ScriptComponents != null)
|
||||||
|
{
|
||||||
|
foreach (var component in ScriptComponents)
|
||||||
|
{
|
||||||
|
if (!component.Enable)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
component.OnDestroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Children != null)
|
||||||
|
{
|
||||||
|
foreach (var child in Children)
|
||||||
|
{
|
||||||
|
child.Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
Children.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Parent?.Children?.Remove(this);
|
||||||
|
Entity.Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class GameObject
|
||||||
|
{
|
||||||
|
// TODO: Implement a more efficient synchronization mechanism for components
|
||||||
|
internal void SyncComponents()
|
||||||
|
{
|
||||||
|
foreach (var (typeHandle, mask) in Scene.World.ComponentStorage.ComponentEntityMasks)
|
||||||
|
{
|
||||||
|
if (!mask.IsSet(Entity.ID))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pool = Scene.World.ComponentStorage.ComponentPools[typeHandle];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SyncScripts()
|
||||||
|
{
|
||||||
|
var scriptsPool = Scene.World.ComponentStorage.ScriptComponentPool.ScriptComponents;
|
||||||
|
if (scriptsPool == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptsPool.TryGetValue(Entity, out var scripts);
|
||||||
|
ScriptComponents = scripts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddComponent<T>(T component)
|
||||||
|
where T : struct, IComponentData
|
||||||
|
{
|
||||||
|
Entity.AddComponent<T>(component);
|
||||||
|
SyncComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveComponent<T>()
|
||||||
|
where T : struct, IComponentData
|
||||||
|
{
|
||||||
|
var result = Entity.RemoveComponent<T>();
|
||||||
|
SyncComponents();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddScript<T>()
|
||||||
|
where T : ScriptComponent, new()
|
||||||
|
{
|
||||||
|
Entity.AddScript<T>();
|
||||||
|
SyncScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddScript(Type type)
|
||||||
|
{
|
||||||
|
Entity.AddScript(type);
|
||||||
|
SyncScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveScript<T>()
|
||||||
|
where T : ScriptComponent
|
||||||
|
{
|
||||||
|
var result = Scene.World.EntityManager.RemoveScript<T>(Entity);
|
||||||
|
SyncScripts();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveScriptAt(int index)
|
||||||
|
{
|
||||||
|
var result = Scene.World.EntityManager.RemoveScriptAt(Entity, index);
|
||||||
|
SyncScripts();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
namespace Ghost.Engine.Models;
|
using Ghost.Entities;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Models;
|
||||||
|
|
||||||
public class Scene
|
public class Scene
|
||||||
{
|
{
|
||||||
private readonly HashSet<GameObject> _rootObjects = new();
|
private readonly HashSet<GameObject> _rootObjects = new();
|
||||||
|
private readonly World _world = World.Create();
|
||||||
|
|
||||||
public IEnumerable<GameObject> RootObjects => _rootObjects;
|
public IEnumerable<GameObject> RootObjects => _rootObjects;
|
||||||
|
public World World => _world;
|
||||||
|
|
||||||
internal Scene()
|
internal Scene()
|
||||||
{
|
{
|
||||||
@@ -14,7 +19,7 @@ public class Scene
|
|||||||
{
|
{
|
||||||
foreach (var gameObject in _rootObjects)
|
foreach (var gameObject in _rootObjects)
|
||||||
{
|
{
|
||||||
gameObject.Start();
|
gameObject.OnEnable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +27,7 @@ public class Scene
|
|||||||
{
|
{
|
||||||
foreach (var gameObject in _rootObjects)
|
foreach (var gameObject in _rootObjects)
|
||||||
{
|
{
|
||||||
|
gameObject.OnDisable();
|
||||||
gameObject.Destroy();
|
gameObject.Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
35
Ghost.Editor/Services/AppStateService.cs
Normal file
35
Ghost.Editor/Services/AppStateService.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Ghost.Editor.AppStates;
|
||||||
|
using Ghost.Editor.Contracts;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Services;
|
||||||
|
|
||||||
|
internal class AppStateService(params IEnumerable<IAppState<StateKey>> states)
|
||||||
|
{
|
||||||
|
private readonly Dictionary<StateKey, IAppState<StateKey>> _states = states.ToDictionary(s => s.StateKy, s => s);
|
||||||
|
private IAppState<StateKey>? _current;
|
||||||
|
|
||||||
|
public async Task TransitionToAsync(StateKey stateKey, object? parameter = null)
|
||||||
|
{
|
||||||
|
var previous = _current;
|
||||||
|
var next = _states[stateKey];
|
||||||
|
|
||||||
|
if (previous != null)
|
||||||
|
{
|
||||||
|
await previous.OnExitingAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await next.OnEnteringAsync(parameter);
|
||||||
|
|
||||||
|
if (previous != null)
|
||||||
|
{
|
||||||
|
await previous.OnExitedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await next.OnEnteredAsync(parameter);
|
||||||
|
|
||||||
|
_current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Ghost.Editor/Services/StackedNotificationService.cs
Normal file
50
Ghost.Editor/Services/StackedNotificationService.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using CommunityToolkit.WinUI.Behaviors;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Services;
|
||||||
|
|
||||||
|
public class StackedNotificationService
|
||||||
|
{
|
||||||
|
private InfoBar? _infoBar;
|
||||||
|
private StackedNotificationsBehavior? _notificationQueue;
|
||||||
|
|
||||||
|
internal void SetReference(InfoBar infoBar, StackedNotificationsBehavior notificationQueue)
|
||||||
|
{
|
||||||
|
_infoBar = infoBar;
|
||||||
|
_notificationQueue = notificationQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ClearReference()
|
||||||
|
{
|
||||||
|
if (_infoBar != null)
|
||||||
|
{
|
||||||
|
_infoBar.IsOpen = false;
|
||||||
|
}
|
||||||
|
_infoBar = null;
|
||||||
|
_notificationQueue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowNotification(string? message, InfoBarSeverity severity, int duration = 5, string? title = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var notification = new Notification
|
||||||
|
{
|
||||||
|
Message = message,
|
||||||
|
Severity = severity,
|
||||||
|
Duration = TimeSpan.FromSeconds(duration),
|
||||||
|
Title = title
|
||||||
|
};
|
||||||
|
|
||||||
|
ShowNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowNotification(Notification notification)
|
||||||
|
{
|
||||||
|
_notificationQueue?.Show(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Ghost.Editor/Themes/Dark.xaml
Normal file
8
Ghost.Editor/Themes/Dark.xaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?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.ThemeDictionaries>
|
||||||
|
<ResourceDictionary x:Key="Dark">
|
||||||
|
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ResourceDictionary.ThemeDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
8
Ghost.Editor/Themes/Light.xaml
Normal file
8
Ghost.Editor/Themes/Light.xaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?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.ThemeDictionaries>
|
||||||
|
<ResourceDictionary x:Key="Light">
|
||||||
|
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ResourceDictionary.ThemeDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
73
Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml
Normal file
73
Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Page
|
||||||
|
x:Class="Ghost.Editor.View.Pages.EngineEditor.ConsolePage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<Grid
|
||||||
|
Grid.Row="0"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<CommandBar Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" DefaultLabelPosition="Collapsed">
|
||||||
|
<CommandBar.PrimaryCommands>
|
||||||
|
<AppBarButton Content="Clear" />
|
||||||
|
<AppBarSeparator />
|
||||||
|
<AppBarToggleButton Width="45">
|
||||||
|
<AppBarToggleButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarToggleButton.Icon>
|
||||||
|
</AppBarToggleButton>
|
||||||
|
<AppBarToggleButton Width="45">
|
||||||
|
<AppBarToggleButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarToggleButton.Icon>
|
||||||
|
</AppBarToggleButton>
|
||||||
|
<AppBarToggleButton Width="45">
|
||||||
|
<AppBarToggleButton.Icon>
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</AppBarToggleButton.Icon>
|
||||||
|
</AppBarToggleButton>
|
||||||
|
</CommandBar.PrimaryCommands>
|
||||||
|
|
||||||
|
<CommandBar.SecondaryCommands>
|
||||||
|
<AppBarToggleButton BorderThickness="0" Label="Clear On Play" />
|
||||||
|
<AppBarToggleButton BorderThickness="0" Label="Show Stack Trace" />
|
||||||
|
</CommandBar.SecondaryCommands>
|
||||||
|
</CommandBar>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Log Content -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="100" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<ListView Grid.Row="0" />
|
||||||
|
<Grid
|
||||||
|
Grid.Row="1"
|
||||||
|
Padding="4"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||||
|
<TextBlock
|
||||||
|
IsTextSelectionEnabled="True"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="Test Log"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
11
Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml.cs
Normal file
11
Ghost.Editor/View/Pages/EngineEditor/ConsolePage.xaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||||
|
|
||||||
|
public sealed partial class ConsolePage : Page
|
||||||
|
{
|
||||||
|
public ConsolePage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml
Normal file
71
Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Page
|
||||||
|
x:Class="Ghost.Editor.View.Pages.EngineEditor.ProjectPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Folder Tree View -->
|
||||||
|
<Grid
|
||||||
|
Grid.Column="0"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
|
||||||
|
BorderThickness="0,0,1,0">
|
||||||
|
<TreeView
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||||
|
<TreeView.ItemContainerStyle>
|
||||||
|
<Style TargetType="TreeViewItem">
|
||||||
|
<Setter Property="Padding" Value="4,2" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,2" />
|
||||||
|
</Style>
|
||||||
|
</TreeView.ItemContainerStyle>
|
||||||
|
</TreeView>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Files -->
|
||||||
|
<ScrollViewer
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<GridView HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||||
|
<GridView.ItemContainerStyle>
|
||||||
|
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
|
||||||
|
<Setter Property="Margin" Value="5,5,5,5" />
|
||||||
|
</Style>
|
||||||
|
</GridView.ItemContainerStyle>
|
||||||
|
|
||||||
|
<GridView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="0.2*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<ImageIcon
|
||||||
|
Grid.Row="0"
|
||||||
|
Width="24"
|
||||||
|
Height="24" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="8,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</GridView.ItemTemplate>
|
||||||
|
</GridView>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
30
Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml.cs
Normal file
30
Ghost.Editor/View/Pages/EngineEditor/ProjectPage.xaml.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||||||
|
using Windows.Foundation;
|
||||||
|
using Windows.Foundation.Collections;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
|
|
||||||
|
// To learn more about WinUI, the WinUI project structure,
|
||||||
|
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||||
|
|
||||||
|
namespace Ghost.Editor.View.Pages.EngineEditor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class ProjectPage : Page
|
||||||
|
{
|
||||||
|
public ProjectPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
xmlns:editor="using:Ghost.Editor.Controls"
|
xmlns:editor="using:Ghost.Editor.Controls"
|
||||||
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
|
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
NavigationCacheMode="Enabled"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
Width="24"
|
Width="24"
|
||||||
Height="24">
|
Height="24">
|
||||||
<ImageIcon.Source>
|
<ImageIcon.Source>
|
||||||
<BitmapImage UriSource="{x:Bind GetIconURI(), Mode=OneWay}" />
|
<BitmapImage UriSource="{x:Bind GetIconURI()}" />
|
||||||
</ImageIcon.Source>
|
</ImageIcon.Source>
|
||||||
</ImageIcon>
|
</ImageIcon>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -76,18 +77,18 @@
|
|||||||
<Grid Grid.Row="0" CornerRadius="4">
|
<Grid Grid.Row="0" CornerRadius="4">
|
||||||
<Image VerticalAlignment="Center" Stretch="UniformToFill">
|
<Image VerticalAlignment="Center" Stretch="UniformToFill">
|
||||||
<Image.Source>
|
<Image.Source>
|
||||||
<BitmapImage UriSource="{x:Bind ViewModel.SelectedTemplate.GetPreviewURI(), Mode=OneWay}" />
|
<BitmapImage UriSource="{x:Bind ViewModel.SelectedTemplate.Value.GetPreviewURI(), Mode=OneWay}" />
|
||||||
</Image.Source>
|
</Image.Source>
|
||||||
</Image>
|
</Image>
|
||||||
<Grid
|
<Grid
|
||||||
HorizontalAlignment="Stretch"
|
MaxHeight="100"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Bottom"
|
||||||
Background="{ThemeResource CircleElevationBorderBrush}">
|
Background="{ThemeResource ControlOnImageFillColorDefaultBrush}">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="16"
|
Margin="16"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Foreground="{ThemeResource TextOnAccentFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||||
Text="{x:Bind ViewModel.SelectedTemplate.Info.Description, Mode=OneWay}" />
|
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Description, Mode=OneWay}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="0,16,0,8"
|
Margin="0,16,0,8"
|
||||||
Style="{StaticResource TitleTextBlockStyle}"
|
Style="{StaticResource TitleTextBlockStyle}"
|
||||||
Text="{x:Bind ViewModel.SelectedTemplate.Info.Name, Mode=OneWay}" />
|
Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Name, Mode=OneWay}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Margin="0,8,0,16"
|
Margin="0,8,0,16"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
|||||||
@@ -3,106 +3,162 @@
|
|||||||
x:Class="Ghost.Editor.View.Pages.Landing.OpenProjectPage"
|
x:Class="Ghost.Editor.View.Pages.Landing.OpenProjectPage"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="using:Ghost.Editor.Helpers.Converters"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:data="using:Ghost.Data.Models"
|
xmlns:data="using:Ghost.Data.Models"
|
||||||
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
|
xmlns:local="using:Ghost.Editor.View.Pages.Landing"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
NavigationCacheMode="Enabled"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Grid>
|
<Page.Resources>
|
||||||
|
<converters:GetDirectoryNameConverter x:Key="DirNameConverter" />
|
||||||
|
</Page.Resources>
|
||||||
|
|
||||||
|
<Grid x:Name="MainContainer">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid Grid.Row="0" Margin="16,8,16,16">
|
<Grid Grid.Row="0" Margin="16,4">
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||||
|
Text="Projects" />
|
||||||
|
<AutoSuggestBox
|
||||||
|
Width="300"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
PlaceholderText="Search project by name"
|
||||||
|
QueryIcon="Find" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Header for the ListView -->
|
||||||
|
<Grid Grid.Row="1" Margin="28,16,45,8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="200" />
|
<ColumnDefinition Width="200" />
|
||||||
<ColumnDefinition Width="200" />
|
<ColumnDefinition Width="165" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="12,0,0,0"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
Text="NAME" />
|
||||||
Text="Name" />
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
HorizontalAlignment="Right"
|
||||||
Text="Last Open" />
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="LAST OPEN" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
HorizontalAlignment="Right"
|
||||||
Text="Engine Version" />
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="ENGINE VERSION" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ListView
|
<!-- Project ListView -->
|
||||||
x:Name="ProjectListView"
|
<Grid
|
||||||
Grid.Row="1"
|
Grid.Row="2"
|
||||||
Padding="8"
|
Padding="8"
|
||||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
AllowDrop="True"
|
||||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
DragEnter="ProjectContainer_DragEnter"
|
||||||
IsItemClickEnabled="True"
|
DragLeave="ProjectContainer_DragLeave"
|
||||||
ItemClick="ListView_ItemClick"
|
DragOver="ProjectContainer_DragOver"
|
||||||
ItemsSource="{x:Bind projects}"
|
Drop="ProjectContainer_Drop">
|
||||||
SelectionMode="None"
|
<ListView
|
||||||
Visibility="Visible">
|
Padding="4,8"
|
||||||
<ListView.ItemTemplate>
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
<DataTemplate x:DataType="data:ProjectInfo">
|
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||||
<Grid Padding="4,8">
|
IsItemClickEnabled="True"
|
||||||
<Grid.ColumnDefinitions>
|
ItemClick="ListView_ItemClick"
|
||||||
<ColumnDefinition Width="*" />
|
ItemsSource="{x:Bind projects}"
|
||||||
<ColumnDefinition Width="200" />
|
SelectionMode="None">
|
||||||
<ColumnDefinition Width="100" />
|
<ListView.ItemTemplate>
|
||||||
<ColumnDefinition Width="100" />
|
<DataTemplate x:DataType="data:ProjectMetadataInfo">
|
||||||
</Grid.ColumnDefinitions>
|
<Grid Height="64" Padding="4,8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="200" />
|
||||||
|
<ColumnDefinition Width="100" />
|
||||||
|
<ColumnDefinition Width="65" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Grid Grid.Column="0">
|
<Grid Grid.Column="0" VerticalAlignment="Center">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="16"
|
||||||
|
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||||
|
Text="{x:Bind Metadata.Name}" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0,4,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind Path, Converter={StaticResource DirNameConverter}}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="0"
|
Grid.Column="1"
|
||||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
Margin="16,4"
|
||||||
Text="{x:Bind Name}" />
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Bind Metadata.LastOpened}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="1"
|
Grid.Column="2"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Margin="16,4"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
HorizontalAlignment="Right"
|
||||||
Text="{x:Bind Path}" />
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Bind Metadata.EngineVersion}" />
|
||||||
|
<Button
|
||||||
|
Grid.Column="3"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0">
|
||||||
|
<FontIcon Glyph="" />
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
<TextBlock
|
<!-- Drag Visual -->
|
||||||
Grid.Column="1"
|
<Grid
|
||||||
Margin="16,4"
|
x:Name="DragVisual"
|
||||||
VerticalAlignment="Center"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{x:Bind LastOpened}" />
|
VerticalAlignment="Stretch"
|
||||||
<TextBlock
|
Background="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
Grid.Column="2"
|
BorderBrush="{ThemeResource ControlStrongStrokeColorDefaultBrush}"
|
||||||
Margin="16,4"
|
BorderThickness="2"
|
||||||
VerticalAlignment="Center"
|
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||||
Text="{x:Bind EngineVersion}" />
|
Visibility="Collapsed">
|
||||||
<Button
|
<TextBlock
|
||||||
Grid.Column="3"
|
HorizontalAlignment="Center"
|
||||||
HorizontalAlignment="Right"
|
VerticalAlignment="Center"
|
||||||
Background="Transparent"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
BorderThickness="0">
|
Style="{StaticResource TitleTextBlockStyle}"
|
||||||
<FontIcon Glyph="" />
|
Text="Drage Project Folder Here" />
|
||||||
</Button>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
|
||||||
</ListView.ItemTemplate>
|
|
||||||
</ListView>
|
|
||||||
|
|
||||||
<TextBlock
|
<!-- Empty Place Holder -->
|
||||||
x:Name="PlaceHolderText"
|
<Grid
|
||||||
HorizontalAlignment="Center"
|
x:Name="EmptyPlaceHolder"
|
||||||
VerticalAlignment="Center"
|
Grid.Row="2"
|
||||||
Style="{StaticResource TitleTextBlockStyle}"
|
Visibility="Collapsed">
|
||||||
Text="No projects found"
|
<TextBlock
|
||||||
Visibility="Collapsed" />
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Style="{StaticResource TitleTextBlockStyle}"
|
||||||
|
Text="No projects found" />
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Page>
|
</Page>
|
||||||
@@ -1,56 +1,133 @@
|
|||||||
using Ghost.Data.Models;
|
using Ghost.Data.Models;
|
||||||
using Ghost.Data.Services;
|
using Ghost.Data.Services;
|
||||||
using Ghost.Editor.View.Windows;
|
using Ghost.Editor.AppStates;
|
||||||
|
using Ghost.Editor.Services;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Navigation;
|
using Microsoft.UI.Xaml.Navigation;
|
||||||
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
// To learn more about WinUI, the WinUI project structure,
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
using Windows.Storage;
|
||||||
|
|
||||||
namespace Ghost.Editor.View.Pages.Landing;
|
namespace Ghost.Editor.View.Pages.Landing;
|
||||||
|
|
||||||
internal sealed partial class OpenProjectPage : Page
|
internal sealed partial class OpenProjectPage : Page
|
||||||
{
|
{
|
||||||
private readonly ProjectService _projectService;
|
private readonly ProjectService _projectService;
|
||||||
|
private readonly StackedNotificationService _notificationService;
|
||||||
|
private readonly AppStateService _stateService;
|
||||||
|
|
||||||
public readonly ObservableCollection<ProjectInfo> projects = new();
|
public readonly ObservableCollection<ProjectMetadataInfo> projects = new();
|
||||||
|
|
||||||
public OpenProjectPage()
|
public OpenProjectPage()
|
||||||
{
|
{
|
||||||
|
_notificationService = App.GetService<StackedNotificationService>();
|
||||||
_projectService = App.GetService<ProjectService>();
|
_projectService = App.GetService<ProjectService>();
|
||||||
|
_stateService = App.GetService<AppStateService>();
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateEmptyPlaceHolderVisibility()
|
||||||
|
{
|
||||||
|
EmptyPlaceHolder.Visibility = projects.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
||||||
{
|
{
|
||||||
await foreach (var project in _projectService.LoadAllProjectAsync())
|
base.OnNavigatedTo(e);
|
||||||
|
|
||||||
|
projects.Clear();
|
||||||
|
await foreach (var projectInfo in _projectService.LoadAllProjectAsync())
|
||||||
{
|
{
|
||||||
projects.Add(project);
|
var metadata = await ProjectService.LoadMetadataAsync(projectInfo.MetadataPath);
|
||||||
|
if (metadata == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
projects.Add(new(projectInfo.MetadataPath, metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projects.Count == 0)
|
UpdateEmptyPlaceHolderVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProjectContainer_DragEnter(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
DragVisual.Visibility = Visibility.Visible;
|
||||||
|
EmptyPlaceHolder.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProjectContainer_DragLeave(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
DragVisual.Visibility = Visibility.Collapsed;
|
||||||
|
UpdateEmptyPlaceHolderVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProjectContainer_DragOver(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.DataView.Contains(StandardDataFormats.StorageItems))
|
||||||
{
|
{
|
||||||
PlaceHolderText.Visibility = Visibility.Visible;
|
e.AcceptedOperation = DataPackageOperation.Link;
|
||||||
ProjectListView.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e.AcceptedOperation = DataPackageOperation.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ProjectContainer_Drop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
var errorMessage = string.Empty;
|
||||||
|
if (e.DataView.Contains(StandardDataFormats.StorageItems))
|
||||||
|
{
|
||||||
|
var items = await e.DataView.GetStorageItemsAsync();
|
||||||
|
var rootFolder = items.OfType<StorageFolder>().FirstOrDefault();
|
||||||
|
if (rootFolder != null)
|
||||||
|
{
|
||||||
|
var result = await _projectService.AddProjectFromDirectoryAsync(rootFolder.Path);
|
||||||
|
if (result.success)
|
||||||
|
{
|
||||||
|
projects.Add(result.data);
|
||||||
|
DragVisual.Visibility = Visibility.Collapsed;
|
||||||
|
goto CloseDropPanel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = result.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = "Unsupported data format. Please drop a folder containing a project.";
|
||||||
|
}
|
||||||
|
|
||||||
|
_notificationService.ShowNotification(errorMessage, InfoBarSeverity.Error);
|
||||||
|
|
||||||
|
CloseDropPanel:
|
||||||
|
DragVisual.Visibility = Visibility.Collapsed;
|
||||||
|
UpdateEmptyPlaceHolderVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.ClickedItem is not ProjectInfo project)
|
if (e.ClickedItem is not ProjectMetadataInfo project)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EngineEditorWindow.TryLoadProject(project))
|
try
|
||||||
{
|
{
|
||||||
App.GetService<LandingWindow>().Close();
|
project.Metadata.LastOpened = DateTime.Now;
|
||||||
|
await ProjectService.CreateMetadataFileAsync(project.Path, project.Metadata);
|
||||||
|
|
||||||
project.LastOpened = System.DateTime.Now;
|
await _stateService.TransitionToAsync(StateKey.EngineEditor, project.Metadata);
|
||||||
await _projectService.UpdateProjectAsync(project);
|
}
|
||||||
|
catch (Exception exp)
|
||||||
|
{
|
||||||
|
_notificationService.ShowNotification($"Failed to load project: {exp.Message}", InfoBarSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,16 +5,18 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:ee="using:Ghost.Editor.View.Pages.EngineEditor"
|
||||||
xmlns:local="using:Ghost.Editor.View.Windows"
|
xmlns:local="using:Ghost.Editor.View.Windows"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:winex="using:WinUIEx"
|
xmlns:winex="using:WinUIEx"
|
||||||
|
Activated="WindowEx_Activated"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<Window.SystemBackdrop>
|
<Window.SystemBackdrop>
|
||||||
<MicaBackdrop />
|
<MicaBackdrop />
|
||||||
</Window.SystemBackdrop>
|
</Window.SystemBackdrop>
|
||||||
|
|
||||||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
Margin="8,0,0,0"
|
Margin="8,0,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind ViewModel.CurrentProject.Name}" />
|
Text="{x:Bind ViewModel.CurrentProject.Name, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
@@ -85,32 +87,41 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Width="350"
|
Width="350"
|
||||||
Background="Aquamarine" />
|
Background="Aquamarine" />
|
||||||
<Grid Grid.Column="1" />
|
<Grid Grid.Column="1">
|
||||||
|
<Image Source="C:\Users\Misaki\OneDrive\Pictures\Screenshots\Screenshot 2024-07-20 021657.png" Stretch="UniformToFill" />
|
||||||
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Width="350"
|
Width="350"
|
||||||
Background="Bisque" />
|
Background="Bisque" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="1" Height="350">
|
<TabView
|
||||||
<Grid.ColumnDefinitions>
|
Grid.Row="1"
|
||||||
<ColumnDefinition Width="*" />
|
Height="350"
|
||||||
<ColumnDefinition Width="Auto" />
|
TabWidthMode="Compact">
|
||||||
</Grid.ColumnDefinitions>
|
<TabView.TabItems>
|
||||||
|
<TabViewItem Header="Project">
|
||||||
<Grid Grid.Column="0" Background="AliceBlue" />
|
<TabViewItem.IconSource>
|
||||||
<Grid
|
<FontIconSource Glyph="" />
|
||||||
Grid.Column="1"
|
</TabViewItem.IconSource>
|
||||||
Width="500"
|
<ee:ProjectPage />
|
||||||
Background="HotPink" />
|
</TabViewItem>
|
||||||
</Grid>
|
<TabViewItem Header="Console">
|
||||||
|
<TabViewItem.IconSource>
|
||||||
|
<FontIconSource Glyph="" />
|
||||||
|
</TabViewItem.IconSource>
|
||||||
|
<ee:ConsolePage />
|
||||||
|
</TabViewItem>
|
||||||
|
</TabView.TabItems>
|
||||||
|
</TabView>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
<Grid
|
<Grid
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
Height="25"
|
Height="25"
|
||||||
Background="{ThemeResource SmokeFillColorDefaultBrush}">
|
Background="{ThemeResource SolidBackgroundFillColorBaseAltBrush}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Ghost.Data.Models;
|
using Ghost.Data.Resources;
|
||||||
using Ghost.Data.Resources;
|
|
||||||
using Ghost.Editor.ViewModel.Windows;
|
using Ghost.Editor.ViewModel.Windows;
|
||||||
using Ghost.Engine.Resources;
|
using Ghost.Engine.Resources;
|
||||||
using WinUIEx;
|
using WinUIEx;
|
||||||
@@ -22,7 +21,7 @@ internal sealed partial class EngineEditorWindow : WindowEx
|
|||||||
{
|
{
|
||||||
ViewModel = App.GetService<EngineEditorViewModel>();
|
ViewModel = App.GetService<EngineEditorViewModel>();
|
||||||
|
|
||||||
AppWindow.SetIcon(AssetsPath.AppIconPath);
|
AppWindow.SetIcon(AssetsPath.s_appIconPath);
|
||||||
Title = EngineData.ENGINE_NAME;
|
Title = EngineData.ENGINE_NAME;
|
||||||
ExtendsContentIntoTitleBar = true;
|
ExtendsContentIntoTitleBar = true;
|
||||||
|
|
||||||
@@ -31,23 +30,8 @@ internal sealed partial class EngineEditorWindow : WindowEx
|
|||||||
this.CenterOnScreen();
|
this.CenterOnScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryLoadProject(ProjectInfo project)
|
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
try
|
Bindings.Update();
|
||||||
{
|
|
||||||
var window = App.GetService<EngineEditorWindow>();
|
|
||||||
window.ViewModel.CurrentProject = project;
|
|
||||||
|
|
||||||
window.Activate();
|
|
||||||
window.Bindings.Update();
|
|
||||||
|
|
||||||
App.SetWindow(window);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (System.Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,14 @@
|
|||||||
x:Class="Ghost.Editor.View.Windows.LandingWindow"
|
x:Class="Ghost.Editor.View.Windows.LandingWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||||
xmlns:local="using:Ghost.Editor.View.Windows"
|
xmlns:local="using:Ghost.Editor.View.Windows"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:winex="using:WinUIEx"
|
xmlns:winex="using:WinUIEx"
|
||||||
|
Activated="WindowEx_Activated"
|
||||||
|
Closed="WindowEx_Closed"
|
||||||
IsResizable="False"
|
IsResizable="False"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
@@ -55,8 +59,19 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Padding="8"
|
Padding="8"
|
||||||
CacheMode="BitmapCache"
|
CacheMode="BitmapCache"
|
||||||
CacheSize="10"
|
CacheSize="10" />
|
||||||
IsNavigationStackEnabled="False" />
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" Padding="16">
|
||||||
|
<InfoBar
|
||||||
|
x:Name="InfoBar"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom">
|
||||||
|
<interactivity:Interaction.Behaviors>
|
||||||
|
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
|
||||||
|
</interactivity:Interaction.Behaviors>
|
||||||
|
</InfoBar>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</winex:WindowEx>
|
</winex:WindowEx>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using Ghost.Data.Resources;
|
using Ghost.Data.Resources;
|
||||||
|
using Ghost.Editor.Services;
|
||||||
using Ghost.Editor.View.Pages.Landing;
|
using Ghost.Editor.View.Pages.Landing;
|
||||||
using Ghost.Engine.Resources;
|
using Ghost.Engine.Resources;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Media.Animation;
|
using Microsoft.UI.Xaml.Media.Animation;
|
||||||
using WinUIEx;
|
using WinUIEx;
|
||||||
@@ -9,11 +11,13 @@ namespace Ghost.Editor.View.Windows;
|
|||||||
|
|
||||||
internal sealed partial class LandingWindow : WindowEx
|
internal sealed partial class LandingWindow : WindowEx
|
||||||
{
|
{
|
||||||
|
private IServiceScope? _landingScope;
|
||||||
|
|
||||||
private int _previousSelectedIndex;
|
private int _previousSelectedIndex;
|
||||||
|
|
||||||
public LandingWindow()
|
public LandingWindow()
|
||||||
{
|
{
|
||||||
AppWindow.SetIcon(AssetsPath.AppIconPath);
|
AppWindow.SetIcon(AssetsPath.s_appIconPath);
|
||||||
Title = EngineData.ENGINE_NAME;
|
Title = EngineData.ENGINE_NAME;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -24,6 +28,19 @@ internal sealed partial class LandingWindow : WindowEx
|
|||||||
ExtendsContentIntoTitleBar = true;
|
ExtendsContentIntoTitleBar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
_landingScope?.Dispose();
|
||||||
|
_landingScope = App.CreateScope();
|
||||||
|
App.GetService<StackedNotificationService>().SetReference(InfoBar, NotificationQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||||
|
{
|
||||||
|
_landingScope?.Dispose();
|
||||||
|
App.GetService<StackedNotificationService>().ClearReference();
|
||||||
|
}
|
||||||
|
|
||||||
private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e)
|
private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
var selectedItem = sender.SelectedItem;
|
var selectedItem = sender.SelectedItem;
|
||||||
@@ -37,7 +54,7 @@ internal sealed partial class LandingWindow : WindowEx
|
|||||||
var slideNavigationTransitionEffect = currentSelectedIndex - _previousSelectedIndex > 0 ?
|
var slideNavigationTransitionEffect = currentSelectedIndex - _previousSelectedIndex > 0 ?
|
||||||
SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft;
|
SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft;
|
||||||
|
|
||||||
ContentFrame.Navigate(pageType, null, new SlideNavigationTransitionInfo() { Effect = slideNavigationTransitionEffect });
|
ContentFrame.Navigate(pageType, _landingScope, new SlideNavigationTransitionInfo() { Effect = slideNavigationTransitionEffect });
|
||||||
|
|
||||||
_previousSelectedIndex = currentSelectedIndex;
|
_previousSelectedIndex = currentSelectedIndex;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Ghost.Editor.ViewModel.Pages.EngineEditor;
|
||||||
|
|
||||||
|
internal class ProjectViewModel
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -2,18 +2,20 @@
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Ghost.Data.Models;
|
using Ghost.Data.Models;
|
||||||
using Ghost.Data.Services;
|
using Ghost.Data.Services;
|
||||||
|
using Ghost.Editor.AppStates;
|
||||||
using Ghost.Editor.Contracts;
|
using Ghost.Editor.Contracts;
|
||||||
using Ghost.Editor.Helpers;
|
using Ghost.Editor.Helpers;
|
||||||
using Ghost.Editor.View.Windows;
|
using Ghost.Editor.Services;
|
||||||
|
using Ghost.Engine.Resources;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.ViewModel.Pages.Landing;
|
namespace Ghost.Editor.ViewModel.Pages.Landing;
|
||||||
|
|
||||||
internal partial class CreateProjectViewModel(ProjectService projectService) : ObservableRecipient, INavigationAware
|
internal partial class CreateProjectViewModel(StackedNotificationService notificationService, ProjectService projectService, AppStateService stateService) : ObservableRecipient, INavigationAware
|
||||||
{
|
{
|
||||||
public ObservableCollection<TemplateData> templates = new();
|
public ObservableCollection<TemplateData> templates = new();
|
||||||
|
|
||||||
@@ -25,14 +27,14 @@ internal partial class CreateProjectViewModel(ProjectService projectService) : O
|
|||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string ProjectName
|
public partial string? ProjectName
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string ProjectLocation
|
public partial string? ProjectLocation
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
@@ -40,7 +42,8 @@ internal partial class CreateProjectViewModel(ProjectService projectService) : O
|
|||||||
|
|
||||||
public async void OnNavigatedTo(object? parameter)
|
public async void OnNavigatedTo(object? parameter)
|
||||||
{
|
{
|
||||||
await foreach (var (path, info) in projectService.GetProjectTemplatesAsync())
|
templates.Clear();
|
||||||
|
await foreach (var (path, info) in ProjectService.GetProjectTemplatesAsync())
|
||||||
{
|
{
|
||||||
templates.Add(new(path, info));
|
templates.Add(new(path, info));
|
||||||
}
|
}
|
||||||
@@ -55,25 +58,39 @@ internal partial class CreateProjectViewModel(ProjectService projectService) : O
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SelectionProjectLocation()
|
private async Task SelectionProjectLocation()
|
||||||
{
|
{
|
||||||
ProjectLocation = (await SystemUtilities.OpenFolderPickerAsync())?.Path ?? string.Empty;
|
var folder = await SystemUtilities.OpenFolderPickerAsync();
|
||||||
|
if (folder != null)
|
||||||
|
{
|
||||||
|
ProjectLocation = folder.Path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task CreateProject()
|
private async Task CreateProject()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(ProjectName) || !Directory.Exists(ProjectLocation) || SelectedTemplate == null)
|
if (string.IsNullOrWhiteSpace(ProjectName)
|
||||||
|
|| !Directory.Exists(ProjectLocation)
|
||||||
|
|| !SelectedTemplate.HasValue)
|
||||||
{
|
{
|
||||||
|
notificationService.ShowNotification("Incorrect project info", InfoBarSeverity.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectPath = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, SelectedTemplate.directory);
|
var result = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, EngineData.s_engineVersion, SelectedTemplate.Value.directory);
|
||||||
|
if (!result.success || result.data == null)
|
||||||
var packageVersion = Package.Current.Id.Version;
|
|
||||||
var newProject = await projectService.AddProjectAsync(ProjectName, projectPath, new System.Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build));
|
|
||||||
|
|
||||||
if (EngineEditorWindow.TryLoadProject(newProject))
|
|
||||||
{
|
{
|
||||||
App.GetService<LandingWindow>().Close();
|
notificationService.ShowNotification(result.message, InfoBarSeverity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = await ProjectService.LoadMetadataAsync(result.data.MetadataPath); // Metadata should not be null here if create project succeeded
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await stateService.TransitionToAsync(StateKey.EngineEditor, metadata);
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
notificationService.ShowNotification($"Failed to load project: {e.Message}", InfoBarSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,10 @@ namespace Ghost.Editor.ViewModel.Windows;
|
|||||||
|
|
||||||
internal partial class EngineEditorViewModel : ObservableRecipient
|
internal partial class EngineEditorViewModel : ObservableRecipient
|
||||||
{
|
{
|
||||||
public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.ENGINE_VERSION}";
|
public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.s_engineVersion}";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ProjectInfo CurrentProject
|
public partial ProjectMetadata CurrentProject
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
using Ghost.Engine.Models;
|
|
||||||
using Ghost.Entities;
|
|
||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace Ghost.Engine;
|
|
||||||
|
|
||||||
public unsafe class GameObject : INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
private readonly Dictionary<Type, ScriptComponent> _components = new();
|
|
||||||
private readonly List<GameObject> _children = new();
|
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
public Entity Entity
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Scene Scene
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
internal set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GameObject? Parent
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
internal set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsActive
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<ScriptComponent> Components => _components.Values;
|
|
||||||
public IEnumerable<GameObject> Children => _children;
|
|
||||||
|
|
||||||
public GameObject(Scene scene, string name)
|
|
||||||
{
|
|
||||||
// TODO: Initialize Entity properly
|
|
||||||
//Entity =
|
|
||||||
Scene = scene;
|
|
||||||
Name = name;
|
|
||||||
IsActive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddComponent<T>(T component)
|
|
||||||
where T : ScriptComponent
|
|
||||||
{
|
|
||||||
_components.Add(typeof(T), component);
|
|
||||||
component.Owner = Entity;
|
|
||||||
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Components)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveComponent<T>()
|
|
||||||
where T : ScriptComponent
|
|
||||||
{
|
|
||||||
var key = typeof(T);
|
|
||||||
if (_components.Remove(key, out var component))
|
|
||||||
{
|
|
||||||
component.OnDestroy();
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Components)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public T? GetComponent<T>()
|
|
||||||
where T : ScriptComponent
|
|
||||||
{
|
|
||||||
if (_components.TryGetValue(typeof(T), out var component))
|
|
||||||
{
|
|
||||||
return (T)component;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddChild(GameObject child)
|
|
||||||
{
|
|
||||||
if (child.Scene != Scene)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Child GameObject must belong to the same Scene.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_children.Add(child);
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveChild(GameObject child)
|
|
||||||
{
|
|
||||||
if (_children.Remove(child))
|
|
||||||
{
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void OnEnable()
|
|
||||||
{
|
|
||||||
foreach (var component in Components)
|
|
||||||
{
|
|
||||||
if (!component.Enable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
component.OnEnable();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in _children)
|
|
||||||
{
|
|
||||||
child.OnEnable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Start()
|
|
||||||
{
|
|
||||||
foreach (var component in Components)
|
|
||||||
{
|
|
||||||
if (!component.Enable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
component.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Update()
|
|
||||||
{
|
|
||||||
foreach (var component in Components)
|
|
||||||
{
|
|
||||||
if (!component.Enable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
component.Update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void LateUpdate()
|
|
||||||
{
|
|
||||||
foreach (var component in Components)
|
|
||||||
{
|
|
||||||
if (!component.Enable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
component.LateUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void FixedUpdate()
|
|
||||||
{
|
|
||||||
foreach (var component in Components)
|
|
||||||
{
|
|
||||||
if (!component.Enable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
component.FixedUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Destroy()
|
|
||||||
{
|
|
||||||
foreach (var component in Components)
|
|
||||||
{
|
|
||||||
if (!component.Enable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
component.OnDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var child in _children)
|
|
||||||
{
|
|
||||||
child.Destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
_children.Clear();
|
|
||||||
_components.Clear();
|
|
||||||
Parent?._children.Remove(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,5 +4,5 @@ internal class EngineData
|
|||||||
{
|
{
|
||||||
public const string ENGINE_NAME = "Ghost Engine";
|
public const string ENGINE_NAME = "Ghost Engine";
|
||||||
|
|
||||||
public readonly static Version ENGINE_VERSION = new(0, 1, 0);
|
public readonly static Version s_engineVersion = new(0, 1, 0);
|
||||||
}
|
}
|
||||||
@@ -10,43 +10,43 @@ internal static class PlayerLoopService
|
|||||||
|
|
||||||
public static void Start()
|
public static void Start()
|
||||||
{
|
{
|
||||||
if (_isRunning)
|
//if (_isRunning)
|
||||||
{
|
//{
|
||||||
return;
|
// return;
|
||||||
}
|
//}
|
||||||
|
|
||||||
foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
//foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
||||||
{
|
//{
|
||||||
gameObject.Start();
|
// gameObject.Start();
|
||||||
}
|
//}
|
||||||
|
|
||||||
_timer ??= new Timer(FixedUpdate, null, 0, (int)(fixedDeltaTime * 1000));
|
//_timer ??= new Timer(FixedUpdate, null, 0, (int)(fixedDeltaTime * 1000));
|
||||||
|
|
||||||
while (_isRunning)
|
//while (_isRunning)
|
||||||
{
|
//{
|
||||||
Update();
|
// Update();
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Update()
|
private static void Update()
|
||||||
{
|
{
|
||||||
foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
//foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
||||||
{
|
//{
|
||||||
gameObject.Update();
|
// gameObject.Update();
|
||||||
}
|
//}
|
||||||
|
|
||||||
foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
//foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
||||||
{
|
//{
|
||||||
gameObject.LateUpdate();
|
// gameObject.LateUpdate();
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void FixedUpdate(object? state)
|
private static void FixedUpdate(object? state)
|
||||||
{
|
{
|
||||||
foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
//foreach (var gameObject in SceneManager.QueryRootGameObjects())
|
||||||
{
|
//{
|
||||||
gameObject.FixedUpdate();
|
// gameObject.FixedUpdate();
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Stop()
|
public static void Stop()
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using Ghost.Engine.Models;
|
namespace Ghost.Engine.Services;
|
||||||
|
|
||||||
namespace Ghost.Engine.Services;
|
|
||||||
|
|
||||||
public enum SceneLoadMode
|
public enum SceneLoadMode
|
||||||
{
|
{
|
||||||
@@ -10,41 +8,41 @@ public enum SceneLoadMode
|
|||||||
|
|
||||||
public static class SceneManager
|
public static class SceneManager
|
||||||
{
|
{
|
||||||
private readonly static HashSet<Scene> _activeScenes = new();
|
//private readonly static HashSet<Scene> _activeScenes = new();
|
||||||
|
|
||||||
internal static IEnumerable<GameObject> QueryRootGameObjects()
|
//internal static IEnumerable<GameObject> QueryRootGameObjects()
|
||||||
{
|
//{
|
||||||
foreach (var scene in _activeScenes)
|
// foreach (var scene in _activeScenes)
|
||||||
{
|
// {
|
||||||
foreach (var gameObject in scene.RootObjects)
|
// foreach (var gameObject in scene.RootObjects)
|
||||||
{
|
// {
|
||||||
if (!gameObject.IsActive)
|
// if (!gameObject.IsActive)
|
||||||
{
|
// {
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
yield return gameObject;
|
// yield return gameObject;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
public static void LoadScene(Scene scene, SceneLoadMode loadMode)
|
//public static void LoadScene(Scene scene, SceneLoadMode loadMode)
|
||||||
{
|
//{
|
||||||
if (loadMode == SceneLoadMode.Single)
|
// if (loadMode == SceneLoadMode.Single)
|
||||||
{
|
// {
|
||||||
foreach (var activeScene in _activeScenes)
|
// foreach (var activeScene in _activeScenes)
|
||||||
{
|
// {
|
||||||
activeScene.Unload();
|
// activeScene.Unload();
|
||||||
}
|
// }
|
||||||
_activeScenes.Clear();
|
// _activeScenes.Clear();
|
||||||
}
|
// }
|
||||||
|
|
||||||
_activeScenes.Add(scene);
|
// _activeScenes.Add(scene);
|
||||||
scene.Load();
|
// scene.Load();
|
||||||
}
|
//}
|
||||||
|
|
||||||
public static Task LoadSceneAsync(Scene scene, SceneLoadMode loadMode)
|
//public static Task LoadSceneAsync(Scene scene, SceneLoadMode loadMode)
|
||||||
{
|
//{
|
||||||
return Task.Run(() => LoadScene(scene, loadMode));
|
// return Task.Run(() => LoadScene(scene, loadMode));
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,5 @@ global using WorldID = System.UInt16;
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Ghost.Engine")]
|
[assembly: InternalsVisibleTo("Ghost.Engine")]
|
||||||
|
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
||||||
[assembly: InternalsVisibleTo("Ghost.Test")]
|
[assembly: InternalsVisibleTo("Ghost.Test")]
|
||||||
@@ -23,11 +23,12 @@ internal interface IComponentPool : IDisposable
|
|||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(Entity entity);
|
public bool Remove(Entity entity);
|
||||||
public bool Has(Entity entity);
|
public bool Has(Entity entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface IComponentPool<T> : IComponentPool
|
internal interface IComponentPool<T> : IComponentPool
|
||||||
|
where T : IComponentData
|
||||||
{
|
{
|
||||||
public void Add(Entity entity, T Component);
|
public void Add(Entity entity, T Component);
|
||||||
}
|
}
|
||||||
@@ -115,8 +116,10 @@ internal class ComponentPool<T> : IComponentPool<T>
|
|||||||
_nextId++;
|
_nextId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(Entity entity)
|
public bool Remove(Entity entity)
|
||||||
{
|
{
|
||||||
|
// We do not remove anything here, the generation of the entity will be used to determine if the component is valid.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ref T GetRef(Entity entity)
|
public ref T GetRef(Entity entity)
|
||||||
@@ -213,13 +216,62 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
|
|||||||
component.Owner = entity;
|
component.Owner = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(Entity entity)
|
public bool Remove<T>(Entity entity)
|
||||||
|
where T : ScriptComponent
|
||||||
{
|
{
|
||||||
if (!Has(entity)
|
if (!Has(entity)
|
||||||
|| !_scriptComponents!.TryGetValue(entity, out var scriptList)
|
|| !_scriptComponents!.TryGetValue(entity, out var scriptList)
|
||||||
|| scriptList == null)
|
|| scriptList == null)
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scriptToRemove = scriptList.FirstOrDefault(script => script is T);
|
||||||
|
if (scriptToRemove == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptToRemove.OnDestroy();
|
||||||
|
scriptList.Remove(scriptToRemove);
|
||||||
|
if (scriptList.Count == 0)
|
||||||
|
{
|
||||||
|
_scriptComponents.Remove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveAt(Entity entity, int index)
|
||||||
|
{
|
||||||
|
if (!Has(entity)
|
||||||
|
|| !_scriptComponents!.TryGetValue(entity, out var scriptList)
|
||||||
|
|| scriptList == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index > scriptList.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptList.RemoveAt(index);
|
||||||
|
if (scriptList.Count == 0)
|
||||||
|
{
|
||||||
|
_scriptComponents.Remove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(Entity entity)
|
||||||
|
{
|
||||||
|
if (!Has(entity)
|
||||||
|
|| !_scriptComponents!.TryGetValue(entity, out var scriptList)
|
||||||
|
|| scriptList == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var script in scriptList)
|
foreach (var script in scriptList)
|
||||||
@@ -228,6 +280,7 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_scriptComponents.Remove(entity);
|
_scriptComponents.Remove(entity);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Has(Entity entity)
|
public bool Has(Entity entity)
|
||||||
@@ -287,7 +340,15 @@ internal class ComponentStorage : IDisposable
|
|||||||
private readonly Dictionary<nint, BitSet> _componentEntityMasks = new();
|
private readonly Dictionary<nint, BitSet> _componentEntityMasks = new();
|
||||||
private readonly ScriptComponentPool _scriptComponentPool = new();
|
private readonly ScriptComponentPool _scriptComponentPool = new();
|
||||||
|
|
||||||
|
private readonly World _world;
|
||||||
|
|
||||||
|
internal ComponentStorage(World world)
|
||||||
|
{
|
||||||
|
_world = world;
|
||||||
|
}
|
||||||
|
|
||||||
internal Dictionary<nint, IComponentPool> ComponentPools => _componentPools;
|
internal Dictionary<nint, IComponentPool> ComponentPools => _componentPools;
|
||||||
|
internal Dictionary<nint, BitSet> ComponentEntityMasks => _componentEntityMasks;
|
||||||
internal ScriptComponentPool ScriptComponentPool => _scriptComponentPool;
|
internal ScriptComponentPool ScriptComponentPool => _scriptComponentPool;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ public class EntityManager : IDisposable
|
|||||||
public int EntityCount => _entities.Count;
|
public int EntityCount => _entities.Count;
|
||||||
public ReadOnlySpan<Entity> Entities => CollectionsMarshal.AsSpan(_entities);
|
public ReadOnlySpan<Entity> Entities => CollectionsMarshal.AsSpan(_entities);
|
||||||
|
|
||||||
|
public event Action<Entity, Type>? OnComponentAdded;
|
||||||
|
public event Action<Entity, Type>? OnComponentRemoved;
|
||||||
|
public event Action<Entity>? OnEntityCreated;
|
||||||
|
public event Action<EntityID>? OnEntityRemoved;
|
||||||
|
|
||||||
internal EntityManager(World world, int initialCapacity)
|
internal EntityManager(World world, int initialCapacity)
|
||||||
{
|
{
|
||||||
_entities = new(initialCapacity);
|
_entities = new(initialCapacity);
|
||||||
@@ -125,17 +130,20 @@ public class EntityManager : IDisposable
|
|||||||
/// <returns>The created <see cref="Entity"/>.</returns>
|
/// <returns>The created <see cref="Entity"/>.</returns>
|
||||||
public Entity CreateEntity()
|
public Entity CreateEntity()
|
||||||
{
|
{
|
||||||
|
Entity entity;
|
||||||
if (_freeEntitySlots.TryDequeue(out var id))
|
if (_freeEntitySlots.TryDequeue(out var id))
|
||||||
{
|
{
|
||||||
return _entities[id];
|
entity = _entities[id];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
id = _entities.Count;
|
id = _entities.Count;
|
||||||
var entity = new Entity(id, 0, _world.ID);
|
entity = new Entity(id, 0, _world.ID);
|
||||||
_entities.Add(entity);
|
_entities.Add(entity);
|
||||||
return entity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OnEntityCreated?.Invoke(entity);
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -149,13 +157,14 @@ public class EntityManager : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_world._componentStorage.Remove(entity);
|
_world.ComponentStorage.Remove(entity);
|
||||||
|
|
||||||
var slot = _entities[entity.ID];
|
var slot = _entities[entity.ID];
|
||||||
slot.IncrementGeneration();
|
slot.IncrementGeneration();
|
||||||
_entities[entity.ID] = slot;
|
_entities[entity.ID] = slot;
|
||||||
_freeEntitySlots.Enqueue(entity.ID);
|
_freeEntitySlots.Enqueue(entity.ID);
|
||||||
|
|
||||||
|
OnEntityRemoved?.Invoke(entity.ID);
|
||||||
entity = Entity.Invalid;
|
entity = Entity.Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,8 +196,9 @@ public class EntityManager : IDisposable
|
|||||||
public void AddComponent<T>(Entity entity, T component)
|
public void AddComponent<T>(Entity entity, T component)
|
||||||
where T : struct, IComponentData
|
where T : struct, IComponentData
|
||||||
{
|
{
|
||||||
_world._componentStorage.GetOrCreateComponentPool<T>().Add(entity, component);
|
_world.ComponentStorage.GetOrCreateComponentPool<T>().Add(entity, component);
|
||||||
_world._componentStorage.GetOrCreateMask(TypeHandle<T>.Value).SetBit(entity.ID);
|
_world.ComponentStorage.GetOrCreateMask(TypeHandle<T>.Value).SetBit(entity.ID);
|
||||||
|
OnComponentAdded?.Invoke(entity, typeof(T));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -197,14 +207,23 @@ public class EntityManager : IDisposable
|
|||||||
/// <typeparam name="T">The type of the component to remove.</typeparam>
|
/// <typeparam name="T">The type of the component to remove.</typeparam>
|
||||||
/// <param name="entity">The entity for which the component is to be remove.</param>
|
/// <param name="entity">The entity for which the component is to be remove.</param>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void RemoveComponent<T>(Entity entity)
|
public bool RemoveComponent<T>(Entity entity)
|
||||||
where T : struct, IComponentData
|
where T : struct, IComponentData
|
||||||
{
|
{
|
||||||
if (_world._componentStorage.TryGetPool<T>(out var pool) && pool.Has(entity))
|
if (!_world.ComponentStorage.TryGetPool<T>(out var pool) || !pool.Has(entity))
|
||||||
{
|
{
|
||||||
pool.Remove(entity);
|
return false;
|
||||||
_world._componentStorage.GetOrCreateMask(TypeHandle<T>.Value).ClearBit(entity.ID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pool.Remove(entity))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_world.ComponentStorage.GetOrCreateMask(TypeHandle<T>.Value).ClearBit(entity.ID);
|
||||||
|
OnComponentRemoved?.Invoke(entity, typeof(T));
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -217,7 +236,7 @@ public class EntityManager : IDisposable
|
|||||||
public void SetComponent<T>(Entity entity, T component)
|
public void SetComponent<T>(Entity entity, T component)
|
||||||
where T : struct, IComponentData
|
where T : struct, IComponentData
|
||||||
{
|
{
|
||||||
_world._componentStorage.GetOrCreateComponentPool<T>().Set(entity, component);
|
_world.ComponentStorage.GetOrCreateComponentPool<T>().Set(entity, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -229,7 +248,7 @@ public class EntityManager : IDisposable
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool HasComponent(Entity entity, nint typeHandle)
|
public bool HasComponent(Entity entity, nint typeHandle)
|
||||||
{
|
{
|
||||||
return _world._componentStorage.TryGetMask(typeHandle, out var bitSet) && bitSet.IsSet(entity.ID);
|
return _world.ComponentStorage.TryGetMask(typeHandle, out var bitSet) && bitSet.IsSet(entity.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -242,7 +261,7 @@ public class EntityManager : IDisposable
|
|||||||
public Ref<T> GetComponent<T>(Entity entity)
|
public Ref<T> GetComponent<T>(Entity entity)
|
||||||
where T : struct, IComponentData
|
where T : struct, IComponentData
|
||||||
{
|
{
|
||||||
if (_world._componentStorage.TryGetPool<T>(out var pool) && pool.Has(entity))
|
if (_world.ComponentStorage.TryGetPool<T>(out var pool) && pool.Has(entity))
|
||||||
{
|
{
|
||||||
return new Ref<T>(ref pool.GetRef(entity));
|
return new Ref<T>(ref pool.GetRef(entity));
|
||||||
}
|
}
|
||||||
@@ -261,7 +280,8 @@ public class EntityManager : IDisposable
|
|||||||
public void AddScript<T>(Entity entity)
|
public void AddScript<T>(Entity entity)
|
||||||
where T : ScriptComponent, new()
|
where T : ScriptComponent, new()
|
||||||
{
|
{
|
||||||
_world._componentStorage.ScriptComponentPool.Add(entity, new T());
|
_world.ComponentStorage.ScriptComponentPool.Add(entity, new T());
|
||||||
|
OnComponentAdded?.Invoke(entity, typeof(ScriptComponent));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -280,7 +300,31 @@ public class EntityManager : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var instance = (ScriptComponent?)Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of {type}.");
|
var instance = (ScriptComponent?)Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of {type}.");
|
||||||
_world._componentStorage.ScriptComponentPool.Add(entity, instance);
|
_world.ComponentStorage.ScriptComponentPool.Add(entity, instance);
|
||||||
|
OnComponentAdded?.Invoke(entity, typeof(ScriptComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveScript<T>(Entity entity)
|
||||||
|
where T : ScriptComponent
|
||||||
|
{
|
||||||
|
if (!_world.ComponentStorage.ScriptComponentPool.Remove<T>(entity))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnComponentRemoved?.Invoke(entity, typeof(ScriptComponent));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveScriptAt(Entity entity, int index)
|
||||||
|
{
|
||||||
|
if (!_world.ComponentStorage.ScriptComponentPool.RemoveAt(entity, index))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnComponentRemoved?.Invoke(entity, typeof(ScriptComponent));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -292,7 +336,7 @@ public class EntityManager : IDisposable
|
|||||||
public T? GetScript<T>(Entity entity)
|
public T? GetScript<T>(Entity entity)
|
||||||
where T : ScriptComponent
|
where T : ScriptComponent
|
||||||
{
|
{
|
||||||
return (T?)_world._componentStorage.ScriptComponentPool.Get(entity)?
|
return (T?)_world.ComponentStorage.ScriptComponentPool.Get(entity)?
|
||||||
.FirstOrDefault(script => script is T tScript);
|
.FirstOrDefault(script => script is T tScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +349,7 @@ public class EntityManager : IDisposable
|
|||||||
public IEnumerable<T> GetScripts<T>(Entity entity)
|
public IEnumerable<T> GetScripts<T>(Entity entity)
|
||||||
where T : ScriptComponent
|
where T : ScriptComponent
|
||||||
{
|
{
|
||||||
return (IEnumerable<T>?)_world._componentStorage.ScriptComponentPool.Get(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty<T>();
|
return (IEnumerable<T>?)_world.ComponentStorage.ScriptComponentPool.Get(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ public static class EntityHelpers
|
|||||||
/// <typeparam name="T">The type of the component to remove.</typeparam>
|
/// <typeparam name="T">The type of the component to remove.</typeparam>
|
||||||
/// <param name="entity">The entity for which the component is to be remove.</param>
|
/// <param name="entity">The entity for which the component is to be remove.</param>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static void RemoveComponent<T>(this Entity entity)
|
public static bool RemoveComponent<T>(this Entity entity)
|
||||||
where T : struct, IComponentData
|
where T : struct, IComponentData
|
||||||
{
|
{
|
||||||
var world = entity.GetWorld();
|
var world = entity.GetWorld();
|
||||||
world.EntityManager.RemoveComponent<T>(entity);
|
return world.EntityManager.RemoveComponent<T>(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ internal struct QueryFilter()
|
|||||||
// Compute All mask (intersection)
|
// Compute All mask (intersection)
|
||||||
foreach (var typeHandle in _all)
|
foreach (var typeHandle in _all)
|
||||||
{
|
{
|
||||||
var mask = world._componentStorage.GetOrCreateMask(typeHandle);
|
var mask = world.ComponentStorage.GetOrCreateMask(typeHandle);
|
||||||
|
|
||||||
if (!hasAll)
|
if (!hasAll)
|
||||||
{
|
{
|
||||||
@@ -52,7 +52,7 @@ internal struct QueryFilter()
|
|||||||
// Compute Any mask (union)
|
// Compute Any mask (union)
|
||||||
foreach (var typeHandle in _any)
|
foreach (var typeHandle in _any)
|
||||||
{
|
{
|
||||||
var mask = world._componentStorage.GetOrCreateMask(typeHandle);
|
var mask = world.ComponentStorage.GetOrCreateMask(typeHandle);
|
||||||
|
|
||||||
if (!hasAny)
|
if (!hasAny)
|
||||||
{
|
{
|
||||||
@@ -66,7 +66,7 @@ internal struct QueryFilter()
|
|||||||
// Compute Absent mask (union for exclusion)
|
// Compute Absent mask (union for exclusion)
|
||||||
foreach (var typeHandle in _absent)
|
foreach (var typeHandle in _absent)
|
||||||
{
|
{
|
||||||
var mask = world._componentStorage.GetOrCreateMask(typeHandle);
|
var mask = world.ComponentStorage.GetOrCreateMask(typeHandle);
|
||||||
|
|
||||||
if (!hasAbsent)
|
if (!hasAbsent)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,14 +2,32 @@
|
|||||||
|
|
||||||
public abstract class ScriptComponent : IComponentData
|
public abstract class ScriptComponent : IComponentData
|
||||||
{
|
{
|
||||||
|
private bool _enable = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether this script component is enabled.
|
/// Gets or sets a value indicating whether this script component is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Enable
|
public bool Enable
|
||||||
{
|
{
|
||||||
get;
|
get => _enable;
|
||||||
set;
|
set
|
||||||
} = true;
|
{
|
||||||
|
if (_enable == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_enable = value;
|
||||||
|
if (_enable)
|
||||||
|
{
|
||||||
|
OnEnable();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnDisable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the entity that owns this script component.
|
/// Gets the entity that owns this script component.
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
namespace Ghost.Entities;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Ghost.Entities;
|
||||||
|
|
||||||
public abstract class SystemBase
|
public abstract class SystemBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the execution order of the current operation or component.
|
||||||
|
/// </summary>
|
||||||
public virtual int ExecutionOrder => 0;
|
public virtual int ExecutionOrder => 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the feature is enabled.
|
||||||
|
/// </summary>
|
||||||
public virtual bool Enable
|
public virtual bool Enable
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = true;
|
} = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The world that this system belongs to.
|
||||||
|
/// </summary>
|
||||||
public World World
|
public World World
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
init;
|
internal set;
|
||||||
} = null!;
|
} = null!;
|
||||||
|
|
||||||
public virtual void OnCreate()
|
public virtual void OnCreate()
|
||||||
@@ -29,38 +40,73 @@ public abstract class SystemBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SystemStorage : IDisposable
|
public class SystemStorage : IDisposable
|
||||||
{
|
{
|
||||||
private readonly List<SystemBase> _systems = new();
|
private readonly List<SystemBase> _systems = new();
|
||||||
private readonly List<SystemBase> _executionList = new();
|
private readonly List<SystemBase> _executionList = new();
|
||||||
|
|
||||||
|
private readonly World _world;
|
||||||
|
|
||||||
|
public event Action<SystemBase>? SystemAdded;
|
||||||
|
public event Action<SystemBase>? SystemRemoved;
|
||||||
|
|
||||||
|
internal SystemStorage(World world)
|
||||||
|
{
|
||||||
|
_world = world;
|
||||||
|
}
|
||||||
|
|
||||||
public void AddSystem<T>(T system)
|
public void AddSystem<T>(T system)
|
||||||
where T : SystemBase
|
where T : SystemBase
|
||||||
{
|
{
|
||||||
_systems.Add(system);
|
_systems.Add(system);
|
||||||
|
system.World = _world;
|
||||||
if (system.Enable)
|
if (system.Enable)
|
||||||
{
|
{
|
||||||
system.OnCreate();
|
system.OnCreate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SystemAdded?.Invoke(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void AddSystem<T>()
|
||||||
|
where T : SystemBase, new()
|
||||||
|
{
|
||||||
|
AddSystem(new T());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveSystem<T>(T system)
|
public void RemoveSystem<T>(T system)
|
||||||
where T : SystemBase
|
where T : SystemBase
|
||||||
{
|
{
|
||||||
|
system.World = null!;
|
||||||
_systems.Remove(system);
|
_systems.Remove(system);
|
||||||
if (system.Enable)
|
if (system.Enable)
|
||||||
{
|
{
|
||||||
system.OnDestroy();
|
system.OnDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SystemRemoved?.Invoke(system);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RebuildExecutionList()
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void RemoveSystem<T>()
|
||||||
|
where T : SystemBase, new()
|
||||||
|
{
|
||||||
|
var system = _systems.FirstOrDefault(s => s is T);
|
||||||
|
if (system != null)
|
||||||
|
{
|
||||||
|
RemoveSystem(system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RebuildExecutionList()
|
||||||
{
|
{
|
||||||
_executionList.Clear();
|
_executionList.Clear();
|
||||||
_executionList.AddRange(_systems.OrderBy(s => s.ExecutionOrder));
|
_executionList.AddRange(_systems.OrderBy(s => s.ExecutionOrder));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSystems()
|
internal void UpdateSystems()
|
||||||
{
|
{
|
||||||
foreach (var system in _systems)
|
foreach (var system in _systems)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
namespace Ghost.Entities.Utilities;
|
|
||||||
|
|
||||||
internal class Box<T>
|
|
||||||
where T : struct
|
|
||||||
{
|
|
||||||
public T Value
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Box(T value)
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static implicit operator T(Box<T> box) => box.Value;
|
|
||||||
public static implicit operator Box<T>(T value) => new(value);
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Ghost.Entities.Utilities;
|
|
||||||
|
|
||||||
internal readonly struct ComponentMask
|
|
||||||
{
|
|
||||||
private readonly ulong[] _words;
|
|
||||||
|
|
||||||
public ComponentMask(int entityCapacity)
|
|
||||||
{
|
|
||||||
_words = new ulong[(entityCapacity + 63) / 64];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Set(int entityIndex)
|
|
||||||
=> _words[entityIndex >> 6] |= 1UL << (entityIndex & 63);
|
|
||||||
|
|
||||||
public void Clear(int entityIndex)
|
|
||||||
=> _words[entityIndex >> 6] &= ~(1UL << (entityIndex & 63));
|
|
||||||
|
|
||||||
public bool Get(int entityIndex)
|
|
||||||
=> ((_words[entityIndex >> 6] >> (entityIndex & 63)) & 1) != 0;
|
|
||||||
|
|
||||||
// Bitwise AND
|
|
||||||
public ComponentMask And(in ComponentMask other)
|
|
||||||
{
|
|
||||||
var result = new ComponentMask(_words.Length * 64);
|
|
||||||
for (var i = 0; i < _words.Length; i++)
|
|
||||||
result._words[i] = _words[i] & other._words[i];
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bitwise OR
|
|
||||||
public ComponentMask Or(in ComponentMask other)
|
|
||||||
{
|
|
||||||
var result = new ComponentMask(_words.Length * 64);
|
|
||||||
for (var i = 0; i < _words.Length; i++)
|
|
||||||
result._words[i] = _words[i] | other._words[i];
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bitwise NOT
|
|
||||||
public ComponentMask Not()
|
|
||||||
{
|
|
||||||
var result = new ComponentMask(_words.Length * 64);
|
|
||||||
for (var i = 0; i < _words.Length; i++)
|
|
||||||
result._words[i] = ~_words[i];
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate set bits (fast scan)
|
|
||||||
public IEnumerable<int> GetEntityIndices()
|
|
||||||
{
|
|
||||||
for (var word = 0; word < _words.Length; word++)
|
|
||||||
{
|
|
||||||
var bits = _words[word];
|
|
||||||
while (bits != 0)
|
|
||||||
{
|
|
||||||
var lowBit = BitOperations.TrailingZeroCount(bits);
|
|
||||||
yield return (word << 6) + lowBit;
|
|
||||||
bits &= bits - 1; // clear lowest set bit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,8 @@ public partial class World
|
|||||||
|
|
||||||
private static int s_maxWorldCount = (int)MathF.Pow(2, Entity.WORLD_INDEX_BITS);
|
private static int s_maxWorldCount = (int)MathF.Pow(2, Entity.WORLD_INDEX_BITS);
|
||||||
|
|
||||||
|
public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count;
|
||||||
|
|
||||||
public static World Create(int entityCapacity = 16)
|
public static World Create(int entityCapacity = 16)
|
||||||
{
|
{
|
||||||
lock (s_worlds)
|
lock (s_worlds)
|
||||||
@@ -46,31 +48,21 @@ public partial class World : IDisposable
|
|||||||
{
|
{
|
||||||
private readonly WorldID _id;
|
private readonly WorldID _id;
|
||||||
private readonly EntityManager _entityManager;
|
private readonly EntityManager _entityManager;
|
||||||
|
private readonly ComponentStorage _componentStorage;
|
||||||
|
private readonly SystemStorage _systemStorage;
|
||||||
|
|
||||||
internal readonly ComponentStorage _componentStorage;
|
internal ComponentStorage ComponentStorage => _componentStorage;
|
||||||
internal readonly SystemStorage _systemStorage;
|
|
||||||
|
|
||||||
public WorldID ID => _id;
|
public WorldID ID => _id;
|
||||||
public EntityManager EntityManager => _entityManager;
|
public EntityManager EntityManager => _entityManager;
|
||||||
|
public SystemStorage SystemStorage => _systemStorage;
|
||||||
|
|
||||||
private World(WorldID id, int entityCapacity)
|
private World(WorldID id, int entityCapacity)
|
||||||
{
|
{
|
||||||
_id = id;
|
_id = id;
|
||||||
_entityManager = new EntityManager(this, entityCapacity);
|
_entityManager = new EntityManager(this, entityCapacity);
|
||||||
_componentStorage = new ComponentStorage();
|
_componentStorage = new ComponentStorage(this);
|
||||||
_systemStorage = new SystemStorage();
|
_systemStorage = new SystemStorage(this);
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public void AddSystem<T>()
|
|
||||||
where T : SystemBase, new()
|
|
||||||
{
|
|
||||||
var instance = new T
|
|
||||||
{
|
|
||||||
World = this
|
|
||||||
};
|
|
||||||
|
|
||||||
_systemStorage.AddSystem(instance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Ghost.Data\Ghost.Data.csproj" />
|
|
||||||
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
|
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
|
||||||
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
|
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
|
||||||
|
<ProjectReference Include="..\Ghost.Graphics\Ghost.Graphics.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ public partial class Test
|
|||||||
entity4.AddComponent(new Mesh { index = 44 });
|
entity4.AddComponent(new Mesh { index = 44 });
|
||||||
entity4.AddScript<UserScript>();
|
entity4.AddScript<UserScript>();
|
||||||
|
|
||||||
world.AddSystem<TestSystem>();
|
world.SystemStorage.AddSystem<TestSystem>();
|
||||||
world._systemStorage.UpdateSystems();
|
world.SystemStorage.UpdateSystems();
|
||||||
|
|
||||||
//world.SystemStorage.RebuildExecutionList();
|
//world.SystemStorage.RebuildExecutionList();
|
||||||
//world.ComponentStorage.RebuildExecutionList();
|
//world.ComponentStorage.RebuildExecutionList();
|
||||||
|
|||||||
Reference in New Issue
Block a user