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 { 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 { 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 { 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 { 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 { 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 { 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): Promise { 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 { 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 { 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 { const current = this.getEnabledPlugins(); if (!current.includes(pluginId)) { await this.setEnabledPlugins([...current, pluginId]); } } /** * 禁用插件 * Disable a plugin */ public async disablePlugin(pluginId: string): Promise { 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 { 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 { const current = this.getDisabledModules(); if (!current.includes(moduleId)) { await this.setDisabledModules([...current, moduleId]); } } /** * 启用模块 * Enable a module */ public async enableModule(moduleId: string): Promise { 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'); } }