Files
esengine/packages/editor-core/src/Services/ProjectService.ts

530 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger, Scene } from '@esengine/ecs-framework';
import { MessageHub } from './MessageHub';
import type { IFileAPI } from '../Types/IFileAPI';
const logger = createLogger('ProjectService');
export type ProjectType = 'esengine' | 'unknown';
export interface ProjectInfo {
path: string;
type: ProjectType;
name: string;
configPath?: string;
}
/**
* UI 设计分辨率配置
* UI Design Resolution Configuration
*/
export interface UIDesignResolution {
/** 设计宽度 / Design width */
width: number;
/** 设计高度 / Design height */
height: number;
}
/**
* 插件配置
* Plugin Configuration
*/
export interface PluginSettings {
/** 启用的插件 ID 列表 / Enabled plugin IDs */
enabledPlugins: string[];
}
/**
* 模块配置
* Module Configuration
*/
export interface ModuleSettings {
/**
* 禁用的模块 ID 列表(黑名单方式)
* Disabled module IDs (blacklist approach)
* Modules NOT in this list are enabled.
* 不在此列表中的模块为启用状态。
*/
disabledModules: string[];
}
export interface ProjectConfig {
projectType?: ProjectType;
/** User scripts directory (default: 'scripts') | 用户脚本目录(默认:'scripts' */
scriptsPath?: string;
/** Build output directory | 构建输出目录 */
buildOutput?: string;
/** Scenes directory | 场景目录 */
scenesPath?: string;
/** Default scene file | 默认场景文件 */
defaultScene?: string;
/** UI design resolution | UI 设计分辨率 */
uiDesignResolution?: UIDesignResolution;
/** Plugin settings | 插件配置 */
plugins?: PluginSettings;
/** Module settings | 模块配置 */
modules?: ModuleSettings;
}
@Injectable()
export class ProjectService implements IService {
private currentProject: ProjectInfo | null = null;
private projectConfig: ProjectConfig | null = null;
private messageHub: MessageHub;
private fileAPI: IFileAPI;
constructor(messageHub: MessageHub, fileAPI: IFileAPI) {
this.messageHub = messageHub;
this.fileAPI = fileAPI;
}
public async createProject(projectPath: string): Promise<void> {
try {
const sep = projectPath.includes('\\') ? '\\' : '/';
const configPath = `${projectPath}${sep}ecs-editor.config.json`;
const configExists = await this.fileAPI.pathExists(configPath);
if (configExists) {
throw new Error('ECS project already exists in this directory');
}
const config: ProjectConfig = {
projectType: 'esengine',
scriptsPath: 'scripts',
buildOutput: '.esengine/compiled',
scenesPath: 'scenes',
defaultScene: 'main.ecs',
plugins: { enabledPlugins: [] },
modules: { disabledModules: [] }
};
await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2));
// Create scenes folder and default scene
// 创建场景文件夹和默认场景
const scenesPath = `${projectPath}${sep}${config.scenesPath}`;
await this.fileAPI.createDirectory(scenesPath);
const defaultScenePath = `${scenesPath}${sep}${config.defaultScene}`;
const emptyScene = new Scene();
const sceneData = emptyScene.serialize({
format: 'json',
pretty: true,
includeMetadata: true
}) as string;
await this.fileAPI.writeFileContent(defaultScenePath, sceneData);
// Create scripts folder for user scripts
// 创建用户脚本文件夹
const scriptsPath = `${projectPath}${sep}${config.scriptsPath}`;
await this.fileAPI.createDirectory(scriptsPath);
// Create scripts/editor folder for editor extension scripts
// 创建编辑器扩展脚本文件夹
const editorScriptsPath = `${scriptsPath}${sep}editor`;
await this.fileAPI.createDirectory(editorScriptsPath);
// Create assets folder for project assets (textures, audio, etc.)
// 创建资源文件夹(纹理、音频等)
const assetsPath = `${projectPath}${sep}assets`;
await this.fileAPI.createDirectory(assetsPath);
// Create tsconfig.json for runtime scripts (components, systems)
// 创建运行时脚本的 tsconfig.json组件、系统等
// Note: paths will be populated by update_project_tsconfig when project is opened
// 注意paths 会在项目打开时由 update_project_tsconfig 填充
const tsConfig = {
compilerOptions: {
target: 'ES2020',
module: 'ESNext',
moduleResolution: 'bundler',
lib: ['ES2020', 'DOM'],
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
experimentalDecorators: true,
emitDecoratorMetadata: true,
noEmit: true
// paths will be added by editor when project is opened
// paths 会在编辑器打开项目时添加
},
include: ['scripts/**/*.ts'],
exclude: ['scripts/editor/**/*.ts', '.esengine']
};
const tsConfigPath = `${projectPath}${sep}tsconfig.json`;
await this.fileAPI.writeFileContent(tsConfigPath, JSON.stringify(tsConfig, null, 2));
// Create tsconfig.editor.json for editor extension scripts
// 创建编辑器扩展脚本的 tsconfig.editor.json
const tsConfigEditor = {
compilerOptions: {
target: 'ES2020',
module: 'ESNext',
moduleResolution: 'bundler',
lib: ['ES2020', 'DOM'],
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
experimentalDecorators: true,
emitDecoratorMetadata: true,
noEmit: true
// paths will be added by editor when project is opened
// paths 会在编辑器打开项目时添加
},
include: ['scripts/editor/**/*.ts'],
exclude: ['.esengine']
};
const tsConfigEditorPath = `${projectPath}${sep}tsconfig.editor.json`;
await this.fileAPI.writeFileContent(tsConfigEditorPath, JSON.stringify(tsConfigEditor, null, 2));
await this.messageHub.publish('project:created', {
path: projectPath
});
logger.info('Project created', { path: projectPath });
} catch (error) {
logger.error('Failed to create project', error);
throw error;
}
}
public async openProject(projectPath: string): Promise<void> {
try {
const projectInfo = await this.validateProject(projectPath);
this.currentProject = projectInfo;
if (projectInfo.configPath) {
this.projectConfig = await this.loadConfig(projectInfo.configPath);
}
await this.messageHub.publish('project:opened', {
path: projectPath,
type: projectInfo.type,
name: projectInfo.name
});
logger.info('Project opened', { path: projectPath, type: projectInfo.type });
} catch (error) {
logger.error('Failed to open project', error);
throw error;
}
}
public async closeProject(): Promise<void> {
if (!this.currentProject) {
logger.warn('No project is currently open');
return;
}
const projectPath = this.currentProject.path;
this.currentProject = null;
this.projectConfig = null;
await this.messageHub.publish('project:closed', { path: projectPath });
logger.info('Project closed', { path: projectPath });
}
public getCurrentProject(): ProjectInfo | null {
return this.currentProject;
}
public getProjectConfig(): ProjectConfig | null {
return this.projectConfig;
}
public isProjectOpen(): boolean {
return this.currentProject !== null;
}
/**
* Get user scripts directory path.
* 获取用户脚本目录路径。
*
* @returns Scripts directory path | 脚本目录路径
*/
public getScriptsPath(): string | null {
if (!this.currentProject) {
return null;
}
const scriptsPath = this.projectConfig?.scriptsPath || 'scripts';
const sep = this.currentProject.path.includes('\\') ? '\\' : '/';
return `${this.currentProject.path}${sep}${scriptsPath}`;
}
/**
* Get editor scripts directory path (scripts/editor).
* 获取编辑器脚本目录路径scripts/editor
*
* @returns Editor scripts directory path | 编辑器脚本目录路径
*/
public getEditorScriptsPath(): string | null {
const scriptsPath = this.getScriptsPath();
if (!scriptsPath) {
return null;
}
const sep = scriptsPath.includes('\\') ? '\\' : '/';
return `${scriptsPath}${sep}editor`;
}
public getScenesPath(): string | null {
if (!this.currentProject) {
return null;
}
const scenesPath = this.projectConfig?.scenesPath || 'assets/scenes';
const sep = this.currentProject.path.includes('\\') ? '\\' : '/';
return `${this.currentProject.path}${sep}${scenesPath}`;
}
public getDefaultScenePath(): string | null {
if (!this.currentProject) {
return null;
}
const scenesPath = this.getScenesPath();
if (!scenesPath) {
return null;
}
const defaultScene = this.projectConfig?.defaultScene || 'main.scene';
const sep = this.currentProject.path.includes('\\') ? '\\' : '/';
return `${scenesPath}${sep}${defaultScene}`;
}
private async validateProject(projectPath: string): Promise<ProjectInfo> {
const projectName = projectPath.split(/[\\/]/).pop() || 'Unknown Project';
const projectInfo: ProjectInfo = {
path: projectPath,
type: 'unknown',
name: projectName
};
const sep = projectPath.includes('\\') ? '\\' : '/';
const configPath = `${projectPath}${sep}ecs-editor.config.json`;
try {
projectInfo.configPath = configPath;
projectInfo.type = 'esengine';
} catch (error) {
logger.warn('No ecs-editor.config.json found, using defaults');
}
return projectInfo;
}
private async loadConfig(configPath: string): Promise<ProjectConfig> {
try {
const content = await this.fileAPI.readFileContent(configPath);
logger.debug('Raw config content:', content);
const config = JSON.parse(content) as ProjectConfig;
logger.debug('Parsed config plugins:', config.plugins);
const result: ProjectConfig = {
projectType: config.projectType || 'esengine',
scriptsPath: config.scriptsPath || 'scripts',
buildOutput: config.buildOutput || '.esengine/compiled',
scenesPath: config.scenesPath || 'scenes',
defaultScene: config.defaultScene || 'main.ecs',
uiDesignResolution: config.uiDesignResolution,
// Provide default empty plugins config for legacy projects
// 为旧项目提供默认的空插件配置
plugins: config.plugins || { enabledPlugins: [] },
modules: config.modules || { disabledModules: [] }
};
logger.debug('Loaded config result:', result);
return result;
} catch (error) {
logger.warn('Failed to load config, using defaults', error);
return {
projectType: 'esengine',
scriptsPath: 'scripts',
buildOutput: '.esengine/compiled',
scenesPath: 'scenes',
defaultScene: 'main.ecs'
};
}
}
/**
* 保存项目配置
*/
public async saveConfig(): Promise<void> {
if (!this.currentProject?.configPath || !this.projectConfig) {
logger.warn('No project or config to save');
return;
}
try {
const content = JSON.stringify(this.projectConfig, null, 2);
await this.fileAPI.writeFileContent(this.currentProject.configPath, content);
logger.info('Project config saved');
} catch (error) {
logger.error('Failed to save project config', error);
throw error;
}
}
/**
* 更新项目配置
*/
public async updateConfig(updates: Partial<ProjectConfig>): Promise<void> {
if (!this.projectConfig) {
logger.warn('No project config to update');
return;
}
this.projectConfig = {
...this.projectConfig,
...updates
};
await this.saveConfig();
await this.messageHub.publish('project:configUpdated', { config: this.projectConfig });
}
/**
* 获取 UI 设计分辨率
* Get UI design resolution
*
* @returns UI design resolution, defaults to 1920x1080 if not set
*/
public getUIDesignResolution(): UIDesignResolution {
return this.projectConfig?.uiDesignResolution || { width: 1920, height: 1080 };
}
/**
* 设置 UI 设计分辨率
* Set UI design resolution
*
* @param resolution - The new design resolution
*/
public async setUIDesignResolution(resolution: UIDesignResolution): Promise<void> {
await this.updateConfig({ uiDesignResolution: resolution });
}
/**
* 获取启用的插件列表
* Get enabled plugins list
*/
public getEnabledPlugins(): string[] {
return this.projectConfig?.plugins?.enabledPlugins || [];
}
/**
* 获取插件配置
* Get plugin settings
*/
public getPluginSettings(): PluginSettings | null {
logger.debug('getPluginSettings called, projectConfig:', this.projectConfig);
logger.debug('getPluginSettings plugins:', this.projectConfig?.plugins);
return this.projectConfig?.plugins || null;
}
/**
* 设置启用的插件列表
* Set enabled plugins list
*
* @param enabledPlugins - Array of enabled plugin IDs
*/
public async setEnabledPlugins(enabledPlugins: string[]): Promise<void> {
await this.updateConfig({
plugins: {
enabledPlugins
}
});
await this.messageHub.publish('project:pluginsChanged', { enabledPlugins });
logger.info('Plugin settings saved', { count: enabledPlugins.length });
}
/**
* 启用插件
* Enable a plugin
*/
public async enablePlugin(pluginId: string): Promise<void> {
const current = this.getEnabledPlugins();
if (!current.includes(pluginId)) {
await this.setEnabledPlugins([...current, pluginId]);
}
}
/**
* 禁用插件
* Disable a plugin
*/
public async disablePlugin(pluginId: string): Promise<void> {
const current = this.getEnabledPlugins();
await this.setEnabledPlugins(current.filter(id => id !== pluginId));
}
// ==================== Module Settings ====================
/**
* 获取禁用的模块列表(黑名单)
* Get disabled modules list (blacklist)
* @returns Array of disabled module IDs
*/
public getDisabledModules(): string[] {
return this.projectConfig?.modules?.disabledModules || [];
}
/**
* 获取模块配置
* Get module settings
*/
public getModuleSettings(): ModuleSettings | null {
return this.projectConfig?.modules || null;
}
/**
* 设置禁用的模块列表
* Set disabled modules list
*
* @param disabledModules - Array of disabled module IDs
*/
public async setDisabledModules(disabledModules: string[]): Promise<void> {
await this.updateConfig({
modules: {
disabledModules
}
});
await this.messageHub.publish('project:modulesChanged', { disabledModules });
logger.info('Module settings saved', { disabledCount: disabledModules.length });
}
/**
* 禁用模块
* Disable a module
*/
public async disableModule(moduleId: string): Promise<void> {
const current = this.getDisabledModules();
if (!current.includes(moduleId)) {
await this.setDisabledModules([...current, moduleId]);
}
}
/**
* 启用模块
* Enable a module
*/
public async enableModule(moduleId: string): Promise<void> {
const current = this.getDisabledModules();
await this.setDisabledModules(current.filter(id => id !== moduleId));
}
/**
* 检查模块是否启用
* Check if a module is enabled
*/
public isModuleEnabled(moduleId: string): boolean {
const disabled = this.getDisabledModules();
return !disabled.includes(moduleId);
}
public dispose(): void {
this.currentProject = null;
this.projectConfig = null;
logger.info('ProjectService disposed');
}
}