fix: 修复项目切换时运行时和系统重复初始化问题 (#267)

* feat(editor): 添加 GitHub Discussions 社区论坛功能

* chore: 更新 pnpm-lock.yaml

* chore: 删除测试图片

* refactor: 改用 Imgur 图床上传图片

* fix: 修复项目切换时运行时和系统重复初始化问题

* fix: 修复多个编辑器问题

* feat: 添加脚本编辑器配置和类型定义支持

* feat: 实现资产 .meta 文件自动生成功能
This commit is contained in:
YHH
2025-12-04 14:04:39 +08:00
committed by GitHub
parent 374b26f7c6
commit b4e7ba2abd
20 changed files with 1040 additions and 75 deletions

View File

@@ -1159,6 +1159,11 @@ export class PluginManager implements IService {
}
}
}
// 重置初始化状态,允许下次重新初始化运行时
// Reset initialized flag to allow re-initialization
this.initialized = false;
logger.debug('Scene systems cleared, runtime can be re-initialized');
}
/**

View File

@@ -20,6 +20,8 @@ import {
IMetaFileSystem,
inferAssetType
} from '@esengine/asset-system-editor';
import type { IFileSystem, FileEntry } from './IFileSystem';
import { IFileSystemService } from './IFileSystem';
// Logger for AssetRegistry using core's logger
const logger = createLogger('AssetRegistry');
@@ -114,6 +116,9 @@ const EXTENSION_TYPE_MAP: Record<string, AssetRegistryType> = {
// Data
'.json': 'json',
'.txt': 'text',
// Scripts
'.ts': 'script',
'.js': 'script',
// Custom types
'.btree': 'btree',
'.ecs': 'scene',
@@ -122,17 +127,8 @@ const EXTENSION_TYPE_MAP: Record<string, AssetRegistryType> = {
'.tsx': 'tileset',
};
/**
* File system interface for asset scanning
*/
interface IFileSystem {
readDir(path: string): Promise<string[]>;
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>;
exists(path: string): Promise<boolean>;
stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean }>;
isDirectory(path: string): Promise<boolean>;
}
// 使用从 IFileSystem.ts 导入的标准接口
// Using standard interface imported from IFileSystem.ts
/**
* Simple in-memory asset database
@@ -243,9 +239,9 @@ export class AssetRegistryService {
async initialize(): Promise<void> {
if (this._initialized) return;
// Get file system service
const IFileSystemServiceKey = Symbol.for('IFileSystemService');
this._fileSystem = Core.services.tryResolve(IFileSystemServiceKey) as IFileSystem | null;
// Get file system service using the exported Symbol
// 使用导出的 Symbol 获取文件系统服务
this._fileSystem = Core.services.tryResolve(IFileSystemService) as IFileSystem | null;
// Get message hub
this._messageHub = Core.services.tryResolve(MessageHub) as MessageHub | null;
@@ -254,10 +250,11 @@ export class AssetRegistryService {
if (this._messageHub) {
this._messageHub.subscribe('project:opened', this._onProjectOpened.bind(this));
this._messageHub.subscribe('project:closed', this._onProjectClosed.bind(this));
} else {
logger.warn('MessageHub not available, cannot subscribe to project events');
}
this._initialized = true;
logger.info('AssetRegistryService initialized');
}
/**
@@ -288,18 +285,16 @@ export class AssetRegistryService {
this._metaManager.clear();
// Setup MetaManager with file system adapter
// 设置 MetaManager 的文件系统适配器
const metaFs: IMetaFileSystem = {
exists: (path: string) => this._fileSystem!.exists(path),
readText: (path: string) => this._fileSystem!.readFile(path),
writeText: (path: string, content: string) => this._fileSystem!.writeFile(path, content),
delete: async (path: string) => {
// Try to delete, ignore if not exists
// Try to delete using deleteFile
// 尝试使用 deleteFile 删除
try {
// Note: IFileSystem may not have delete, handle gracefully
const fs = this._fileSystem as IFileSystem & { delete?: (p: string) => Promise<void> };
if (fs.delete) {
await fs.delete(path);
}
await this._fileSystem!.deleteFile(path);
} catch {
// Ignore delete errors
}
@@ -398,53 +393,61 @@ export class AssetRegistryService {
}
/**
* Scan assets directory and register all assets
* Scan all project directories for assets
* 扫描项目中所有目录的资产
*/
private async _scanAssetsDirectory(): Promise<void> {
if (!this._fileSystem || !this._projectPath) return;
const sep = this._projectPath.includes('\\') ? '\\' : '/';
const assetsPath = `${this._projectPath}${sep}assets`;
try {
const exists = await this._fileSystem.exists(assetsPath);
if (!exists) {
logger.info('No assets directory found');
return;
// 扫描多个目录assets, scripts, scenes
// Scan multiple directories: assets, scripts, scenes
const directoriesToScan = [
{ path: `${this._projectPath}${sep}assets`, name: 'assets' },
{ path: `${this._projectPath}${sep}scripts`, name: 'scripts' },
{ path: `${this._projectPath}${sep}scenes`, name: 'scenes' }
];
for (const dir of directoriesToScan) {
try {
const exists = await this._fileSystem.exists(dir.path);
if (!exists) continue;
await this._scanDirectory(dir.path, dir.name);
} catch (error) {
logger.error(`Failed to scan ${dir.name} directory:`, error);
}
await this._scanDirectory(assetsPath, 'assets');
} catch (error) {
logger.error('Failed to scan assets directory:', error);
}
}
/**
* Recursively scan a directory
* 递归扫描目录
*/
private async _scanDirectory(absolutePath: string, relativePath: string): Promise<void> {
if (!this._fileSystem) return;
try {
const entries = await this._fileSystem.readDir(absolutePath);
// 使用标准 IFileSystem.listDirectory
// Use standard IFileSystem.listDirectory
const entries: FileEntry[] = await this._fileSystem.listDirectory(absolutePath);
const sep = absolutePath.includes('\\') ? '\\' : '/';
for (const entry of entries) {
const entryAbsPath = `${absolutePath}${sep}${entry}`;
const entryRelPath = `${relativePath}/${entry}`;
const entryAbsPath = entry.path || `${absolutePath}${sep}${entry.name}`;
const entryRelPath = `${relativePath}/${entry.name}`;
try {
const isDir = await this._fileSystem.isDirectory(entryAbsPath);
if (isDir) {
if (entry.isDirectory) {
// Recursively scan subdirectory
await this._scanDirectory(entryAbsPath, entryRelPath);
} else {
// Register file as asset
await this._registerAssetFile(entryAbsPath, entryRelPath);
// Register file as asset with size from entry
await this._registerAssetFile(entryAbsPath, entryRelPath, entry.size, entry.modified);
}
} catch (error) {
logger.warn(`Failed to process entry ${entry}:`, error);
logger.warn(`Failed to process entry ${entry.name}:`, error);
}
}
} catch (error) {
@@ -454,8 +457,19 @@ export class AssetRegistryService {
/**
* Register a single asset file
* 注册单个资产文件
*
* @param absolutePath - 绝对路径 | Absolute path
* @param relativePath - 相对路径 | Relative path
* @param size - 文件大小(可选)| File size (optional)
* @param modified - 修改时间(可选)| Modified time (optional)
*/
private async _registerAssetFile(absolutePath: string, relativePath: string): Promise<void> {
private async _registerAssetFile(
absolutePath: string,
relativePath: string,
size?: number,
modified?: Date
): Promise<void> {
if (!this._fileSystem || !this._manifest) return;
// Skip .meta files
@@ -471,18 +485,16 @@ export class AssetRegistryService {
// Skip unknown file types
if (!assetType || assetType === 'binary') return;
// Get file info
let stat: { size: number; mtime: number };
try {
stat = await this._fileSystem.stat(absolutePath);
} catch {
return;
}
// Use provided size/modified or default values
const fileSize = size ?? 0;
const fileMtime = modified ? modified.getTime() : Date.now();
// Use MetaManager to get or create meta (with .meta file)
let meta: IAssetMeta;
try {
logger.debug(`Creating/loading meta for: ${relativePath}`);
meta = await this._metaManager.getOrCreateMeta(absolutePath);
logger.debug(`Meta created/loaded for ${relativePath}: guid=${meta.guid}`);
} catch (e) {
logger.warn(`Failed to get meta for ${relativePath}:`, e);
return;
@@ -510,9 +522,9 @@ export class AssetRegistryService {
path: relativePath,
type: assetType,
name,
size: stat.size,
size: fileSize,
hash: '', // Could compute hash if needed
lastModified: stat.mtime
lastModified: fileMtime
};
// Register in database

View File

@@ -94,11 +94,15 @@ export class ProjectService implements IService {
scriptsPath: 'scripts',
buildOutput: '.esengine/compiled',
scenesPath: 'scenes',
defaultScene: 'main.ecs'
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);
@@ -111,6 +115,55 @@ export class ProjectService implements IService {
}) 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 types folder for type definitions
// 创建类型定义文件夹
const typesPath = `${projectPath}${sep}types`;
await this.fileAPI.createDirectory(typesPath);
// Create tsconfig.json for TypeScript support
// 创建 tsconfig.json 用于 TypeScript 支持
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,
// Reference local type definitions
// 引用本地类型定义文件
typeRoots: ['./types'],
paths: {
'@esengine/ecs-framework': ['./types/ecs-framework.d.ts'],
'@esengine/engine-core': ['./types/engine-core.d.ts']
}
},
include: ['scripts/**/*.ts'],
exclude: ['.esengine']
};
const tsConfigPath = `${projectPath}${sep}tsconfig.json`;
await this.fileAPI.writeFileContent(tsConfigPath, JSON.stringify(tsConfig, null, 2));
await this.messageHub.publish('project:created', {
path: projectPath
});
@@ -258,8 +311,10 @@ export class ProjectService implements IService {
scenesPath: config.scenesPath || 'scenes',
defaultScene: config.defaultScene || 'main.ecs',
uiDesignResolution: config.uiDesignResolution,
plugins: config.plugins,
modules: config.modules
// Provide default empty plugins config for legacy projects
// 为旧项目提供默认的空插件配置
plugins: config.plugins || { enabledPlugins: [] },
modules: config.modules || { disabledModules: [] }
};
logger.debug('Loaded config result:', result);
return result;