setRenameDialog(null)}>
@@ -1518,9 +1914,9 @@ export class ${className} {
try {
const content = await template.getContent(fileName);
await TauriAPI.writeFileContent(filePath, content);
- if (currentPath) {
- await loadAssets(currentPath);
- }
+
+ // Refresh both assets view and folder tree
+ await refreshAll();
// Notify that a file was created | 通知文件已创建
messageHub?.publish('file:created', { path: filePath });
diff --git a/packages/editor-app/src/components/dialogs/AssetPickerDialog.css b/packages/editor-app/src/components/dialogs/AssetPickerDialog.css
index 55a6c91e..86e78de3 100644
--- a/packages/editor-app/src/components/dialogs/AssetPickerDialog.css
+++ b/packages/editor-app/src/components/dialogs/AssetPickerDialog.css
@@ -313,3 +313,40 @@
.asset-save-new-folder button:last-child:hover {
background: #444;
}
+
+/* ==================== Managed Directory Styles ==================== */
+.asset-picker-item.managed-root .asset-picker-item__icon {
+ color: #4fc1ff;
+}
+
+.asset-picker-item.managed-root .managed-icon {
+ color: #4fc1ff;
+}
+
+.asset-picker-item .managed-badge {
+ font-size: 9px;
+ padding: 1px 4px;
+ background: #4fc1ff22;
+ color: #4fc1ff;
+ border-radius: 3px;
+ margin-left: auto;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+/* Disabled items (no GUID) */
+.asset-picker-item.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.asset-picker-item.disabled:hover {
+ background: transparent;
+}
+
+.asset-picker-item .no-guid-badge {
+ margin-left: auto;
+ color: #f59e0b;
+ display: flex;
+ align-items: center;
+}
diff --git a/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx b/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx
index 70d7a927..3d286a33 100644
--- a/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx
+++ b/packages/editor-app/src/components/dialogs/AssetPickerDialog.tsx
@@ -1,10 +1,18 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
-import { X, Search, Folder, FolderOpen, File, Image, FileText, Music, Video } from 'lucide-react';
+import { X, Search, Folder, FolderOpen, File, Image, FileText, Music, Video, Database, AlertTriangle } from 'lucide-react';
import { Core } from '@esengine/ecs-framework';
-import { ProjectService } from '@esengine/editor-core';
+import { ProjectService, AssetRegistryService } from '@esengine/editor-core';
import { TauriFileSystemService } from '../../services/TauriFileSystemService';
import './AssetPickerDialog.css';
+/**
+ * Directories managed by asset registry (GUID system)
+ * Only files in these directories can be selected
+ *
+ * Note: Keep in sync with MANAGED_ASSET_DIRECTORIES in AssetRegistryService.ts
+ */
+const MANAGED_ASSET_DIRECTORIES = ['assets', 'scripts', 'scenes'] as const;
+
interface AssetPickerDialogProps {
isOpen: boolean;
onClose: () => void;
@@ -19,6 +27,10 @@ interface FileNode {
path: string;
isDirectory: boolean;
children?: FileNode[];
+ /** Asset GUID (only for files with registered GUIDs) */
+ guid?: string;
+ /** Whether this is a root managed directory */
+ isRootManaged?: boolean;
}
export function AssetPickerDialog({
@@ -35,7 +47,12 @@ export function AssetPickerDialog({
const [assets, setAssets] = useState
([]);
const [loading, setLoading] = useState(false);
- // Load project assets
+ // Get AssetRegistryService for GUID lookup
+ const assetRegistry = useMemo(() => {
+ return Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
+ }, []);
+
+ // Load project assets - ONLY from managed directories (assets, scripts, scenes)
useEffect(() => {
if (!isOpen) return;
@@ -48,13 +65,44 @@ export function AssetPickerDialog({
const currentProject = projectService?.getCurrentProject();
if (projectService && currentProject) {
const projectPath = currentProject.path;
- const assetsPath = `${projectPath}/assets`;
+ const normalizedProjectPath = projectPath.replace(/\\/g, '/');
+
+ // 排除的目录名 | Excluded directory names
+ const excludedDirs = new Set([
+ 'node_modules', '.git', '.idea', '.vscode', 'dist', 'build',
+ 'temp', 'tmp', '.cache', 'coverage', '__pycache__'
+ ]);
+
+ // Helper to get relative path from absolute path
+ const getRelativePath = (absPath: string): string => {
+ const normalizedAbs = absPath.replace(/\\/g, '/');
+ if (normalizedAbs.startsWith(normalizedProjectPath)) {
+ return normalizedAbs.substring(normalizedProjectPath.length + 1);
+ }
+ return absPath;
+ };
const buildTree = async (dirPath: string): Promise => {
const entries = await fileSystem.listDirectory(dirPath);
const nodes: FileNode[] = [];
for (const entry of entries) {
+ // 跳过排除的目录 | Skip excluded directories
+ if (entry.isDirectory && excludedDirs.has(entry.name)) {
+ continue;
+ }
+
+ // 跳过隐藏文件/目录(以.开头,除了当前目录)
+ // Skip hidden files/directories (starting with ., except current dir)
+ if (entry.name.startsWith('.') && entry.name !== '.') {
+ continue;
+ }
+
+ // Skip .meta files
+ if (entry.name.endsWith('.meta')) {
+ continue;
+ }
+
const node: FileNode = {
name: entry.name,
path: entry.path,
@@ -67,6 +115,15 @@ export function AssetPickerDialog({
} catch {
node.children = [];
}
+ } else {
+ // Try to get GUID for the file
+ if (assetRegistry) {
+ const relativePath = getRelativePath(entry.path);
+ const guid = assetRegistry.getGuidByPath(relativePath);
+ if (guid) {
+ node.guid = guid;
+ }
+ }
}
nodes.push(node);
@@ -80,8 +137,33 @@ export function AssetPickerDialog({
});
};
- const tree = await buildTree(assetsPath);
- setAssets(tree);
+ // Only load managed directories (assets, scripts, scenes)
+ const sep = projectPath.includes('\\') ? '\\' : '/';
+ const managedNodes: FileNode[] = [];
+
+ for (const dirName of MANAGED_ASSET_DIRECTORIES) {
+ const dirPath = `${projectPath}${sep}${dirName}`;
+ try {
+ const exists = await fileSystem.exists(dirPath);
+ if (exists) {
+ const children = await buildTree(dirPath);
+ managedNodes.push({
+ name: dirName,
+ path: dirPath,
+ isDirectory: true,
+ children,
+ isRootManaged: true
+ });
+ }
+ } catch {
+ // Directory doesn't exist, skip
+ }
+ }
+
+ setAssets(managedNodes);
+
+ // Auto-expand managed directories
+ setExpandedFolders(new Set(managedNodes.map(n => n.path)));
}
} catch (error) {
console.error('Failed to load assets:', error);
@@ -93,7 +175,7 @@ export function AssetPickerDialog({
loadAssets();
setSelectedPath(null);
setSearchTerm('');
- }, [isOpen]);
+ }, [isOpen, assetRegistry]);
// Filter assets based on search and file extensions
const filteredAssets = useMemo(() => {
@@ -141,11 +223,19 @@ export function AssetPickerDialog({
});
}, []);
+ // Track selected node (to check for GUID)
+ const [selectedNode, setSelectedNode] = useState(null);
+
const handleSelect = useCallback((node: FileNode) => {
if (node.isDirectory) {
toggleFolder(node.path);
} else {
- setSelectedPath(node.path);
+ // Only allow selecting files with GUID
+ if (node.guid) {
+ setSelectedPath(node.path);
+ setSelectedNode(node);
+ }
+ // Files without GUID cannot be selected
}
}, [toggleFolder]);
@@ -172,11 +262,15 @@ export function AssetPickerDialog({
}, [selectedPath, onSelect, onClose, toRelativePath]);
const handleDoubleClick = useCallback((node: FileNode) => {
- if (!node.isDirectory) {
+ if (!node.isDirectory && node.guid) {
+ // Double-click on file with GUID selects it
onSelect(toRelativePath(node.path));
onClose();
+ } else if (node.isDirectory) {
+ // Double-click on folder toggles expansion
+ toggleFolder(node.path);
}
- }, [onSelect, onClose, toRelativePath]);
+ }, [onSelect, onClose, toRelativePath, toggleFolder]);
const getFileIcon = (name: string) => {
const ext = name.split('.').pop()?.toLowerCase();
@@ -206,23 +300,38 @@ export function AssetPickerDialog({
const renderNode = (node: FileNode, depth: number = 0) => {
const isExpanded = expandedFolders.has(node.path);
const isSelected = selectedPath === node.path;
+ const hasGuid = node.isDirectory || !!node.guid;
+ const isDisabled = !node.isDirectory && !node.guid;
return (
handleSelect(node)}
onDoubleClick={() => handleDoubleClick(node)}
+ title={isDisabled ? 'This file has no GUID and cannot be referenced' : undefined}
>
{node.isDirectory ? (
- isExpanded ? :
+ node.isRootManaged ? (
+
+ ) : (
+ isExpanded ? :
+ )
) : (
getFileIcon(node.name)
)}
{node.name}
+ {node.isRootManaged && (
+
GUID
+ )}
+ {isDisabled && (
+
+
+
+ )}
{node.isDirectory && isExpanded && node.children && (
diff --git a/packages/editor-app/src/components/inspectors/views/AssetFileInspector.tsx b/packages/editor-app/src/components/inspectors/views/AssetFileInspector.tsx
index 3ad61178..2c73f350 100644
--- a/packages/editor-app/src/components/inspectors/views/AssetFileInspector.tsx
+++ b/packages/editor-app/src/components/inspectors/views/AssetFileInspector.tsx
@@ -1,5 +1,9 @@
-import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive } from 'lucide-react';
+import { useState, useEffect, useCallback } from 'react';
+import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive, Settings2 } from 'lucide-react';
import { convertFileSrc } from '@tauri-apps/api/core';
+import { Core } from '@esengine/ecs-framework';
+import { AssetRegistryService } from '@esengine/editor-core';
+import { assetManager as globalAssetManager } from '@esengine/asset-system';
import { AssetFileInfo } from '../types';
import { ImagePreview, CodePreview, getLanguageFromExtension } from '../common';
import '../../../styles/EntityInspector.css';
@@ -10,6 +14,18 @@ interface AssetFileInspectorProps {
isImage?: boolean;
}
+/**
+ * Built-in loader types (always available)
+ * 内置加载器类型(始终可用)
+ */
+const BUILTIN_LOADER_TYPES = [
+ 'texture',
+ 'audio',
+ 'json',
+ 'text',
+ 'binary'
+];
+
function formatFileSize(bytes?: number): string {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
@@ -38,6 +54,68 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
const IconComponent = fileInfo.isDirectory ? Folder : isImage ? ImageIcon : FileIcon;
const iconColor = fileInfo.isDirectory ? '#dcb67a' : isImage ? '#a78bfa' : '#90caf9';
+ // State for loader type selector
+ const [currentLoaderType, setCurrentLoaderType] = useState
(null);
+ const [availableLoaderTypes, setAvailableLoaderTypes] = useState([]);
+ const [detectedType, setDetectedType] = useState(null);
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ // Load meta info and available loader types
+ useEffect(() => {
+ if (fileInfo.isDirectory) return;
+
+ const loadMetaInfo = async () => {
+ try {
+ const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
+ if (!assetRegistry?.isReady) return;
+
+ const metaManager = assetRegistry.metaManager;
+ const meta = await metaManager.getOrCreateMeta(fileInfo.path);
+
+ // Get current loader type from meta
+ setCurrentLoaderType(meta.loaderType || null);
+ setDetectedType(meta.type);
+
+ // Get available loader types from assetManager
+ const loaderFactory = globalAssetManager.getLoaderFactory();
+ const registeredTypes = loaderFactory?.getRegisteredTypes() || [];
+
+ // Combine built-in types with registered types (deduplicated)
+ const allTypes = new Set([...BUILTIN_LOADER_TYPES, ...registeredTypes]);
+ setAvailableLoaderTypes(Array.from(allTypes).sort());
+ } catch (error) {
+ console.warn('Failed to load meta info:', error);
+ }
+ };
+
+ loadMetaInfo();
+ }, [fileInfo.path, fileInfo.isDirectory]);
+
+ // Handle loader type change
+ const handleLoaderTypeChange = useCallback(async (newType: string) => {
+ if (fileInfo.isDirectory || isUpdating) return;
+
+ setIsUpdating(true);
+ try {
+ const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
+ if (!assetRegistry?.isReady) return;
+
+ const metaManager = assetRegistry.metaManager;
+
+ // Update meta with new loader type
+ // Empty string means use auto-detection (remove override)
+ const loaderType = newType === '' ? undefined : newType;
+ await metaManager.updateMeta(fileInfo.path, { loaderType });
+
+ setCurrentLoaderType(loaderType || null);
+ console.log(`[AssetFileInspector] Updated loader type for ${fileInfo.name}: ${loaderType || '(auto)'}`);
+ } catch (error) {
+ console.error('Failed to update loader type:', error);
+ } finally {
+ setIsUpdating(false);
+ }
+ }, [fileInfo.path, fileInfo.name, fileInfo.isDirectory, isUpdating]);
+
return (
@@ -92,6 +170,56 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
+ {/* Loader Type Section - only for files, not directories */}
+ {!fileInfo.isDirectory && availableLoaderTypes.length > 0 && (
+
+
+
+ 加载设置
+
+
+
+
+
+ {currentLoaderType && (
+
+ 已覆盖自动检测,使用 "{currentLoaderType}" 加载器
+
+ )}
+
+ )}
+
{isImage && (
图片预览
diff --git a/packages/editor-app/src/services/EngineService.ts b/packages/editor-app/src/services/EngineService.ts
index 1c96d0ef..5321b010 100644
--- a/packages/editor-app/src/services/EngineService.ts
+++ b/packages/editor-app/src/services/EngineService.ts
@@ -19,7 +19,9 @@ import {
AssetPathResolver,
AssetPlatform,
globalPathResolver,
- SceneResourceManager
+ SceneResourceManager,
+ assetManager as globalAssetManager,
+ AssetType
} from '@esengine/asset-system';
import {
GameRuntime,
@@ -202,12 +204,18 @@ export class EngineService {
engineBridge: this._runtime.bridge,
renderSystem: this._runtime.renderSystem,
assetManager: this._assetManager,
- isEditor: true
+ engineIntegration: this._engineIntegration,
+ isEditor: true,
+ transformType: TransformComponent
};
// 让插件为场景创建系统
pluginManager.createSystemsForScene(this._runtime.scene!, context);
+ // Re-sync assets after plugins registered their loaders
+ // 插件注册完加载器后,重新同步资产(确保类型正确)
+ await this._syncAssetRegistryToManager();
+
// 同步系统引用到 GameRuntime 的 systemContext(用于 start/stop 时启用/禁用系统)
this._runtime.updateSystemContext({
animatorSystem: context.animatorSystem,
@@ -357,7 +365,9 @@ export class EngineService {
*/
private async _initializeAssetSystem(): Promise
{
try {
- this._assetManager = new AssetManager();
+ // Use global assetManager instance so all systems share the same manager
+ // 使用全局 assetManager 实例,以便所有系统共享同一个管理器
+ this._assetManager = globalAssetManager;
// Set up asset reader for Tauri environment.
// 为 Tauri 环境设置资产读取器。
@@ -374,6 +384,10 @@ export class EngineService {
}
}
+ // Sync AssetRegistryService data to global assetManager's database
+ // 将 AssetRegistryService 的数据同步到全局 assetManager 的数据库
+ await this._syncAssetRegistryToManager();
+
const pathTransformerFn = (path: string) => {
if (!path.startsWith('http://') && !path.startsWith('https://') &&
!path.startsWith('data:') && !path.startsWith('asset://')) {
@@ -431,6 +445,97 @@ export class EngineService {
}
}
+ /**
+ * Sync AssetRegistryService data to AssetManager's database.
+ * 将 AssetRegistryService 的数据同步到 AssetManager 的数据库。
+ *
+ * This enables GUID-based asset loading through the global assetManager.
+ * Components like ParticleSystemComponent use the global assetManager to load assets by GUID.
+ *
+ * Asset type resolution order:
+ * 1. loaderType from .meta file (explicit user override)
+ * 2. loaderFactory.getAssetTypeByPath (plugin-registered loaders)
+ * 3. Extension-based fallback (built-in types)
+ *
+ * 资产类型解析顺序:
+ * 1. .meta 文件中的 loaderType(用户显式覆盖)
+ * 2. loaderFactory.getAssetTypeByPath(插件注册的加载器)
+ * 3. 基于扩展名的回退(内置类型)
+ */
+ private async _syncAssetRegistryToManager(): Promise {
+ if (!this._assetManager) return;
+
+ const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
+ if (!assetRegistry || !assetRegistry.isReady) {
+ console.warn('[EngineService] AssetRegistryService not ready, skipping sync');
+ return;
+ }
+
+ const database = this._assetManager.getDatabase();
+ const allAssets = assetRegistry.getAllAssets();
+ const metaManager = assetRegistry.metaManager;
+
+ console.log(`[EngineService] Syncing ${allAssets.length} assets from AssetRegistry to AssetManager`);
+
+ // Use loaderFactory to determine asset type from path
+ // This allows plugins to register their own loaders and types
+ // 使用 loaderFactory 根据路径确定资产类型
+ // 这允许插件注册自己的加载器和类型
+ const loaderFactory = this._assetManager.getLoaderFactory();
+
+ for (const asset of allAssets) {
+ let assetType: string | null = null;
+
+ // 1. Check for explicit loaderType in .meta file (user override)
+ // 1. 检查 .meta 文件中的显式 loaderType(用户覆盖)
+ const meta = metaManager.getMetaByGUID(asset.guid);
+ if (meta?.loaderType) {
+ assetType = meta.loaderType;
+ }
+
+ // 2. Try to get type from registered loaders
+ // 2. 尝试从已注册的加载器获取类型
+ if (!assetType) {
+ assetType = loaderFactory?.getAssetTypeByPath?.(asset.path) ?? null;
+ }
+
+ // 3. Fallback: determine type from extension for basic types
+ // 3. 回退:根据扩展名确定基本类型
+ if (!assetType) {
+ const ext = asset.path.substring(asset.path.lastIndexOf('.')).toLowerCase();
+ if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext)) {
+ assetType = AssetType.Texture;
+ } else if (['.mp3', '.wav', '.ogg', '.m4a'].includes(ext)) {
+ assetType = AssetType.Audio;
+ } else if (['.json'].includes(ext)) {
+ assetType = AssetType.Json;
+ } else if (['.txt', '.md', '.xml', '.yaml'].includes(ext)) {
+ assetType = AssetType.Text;
+ } else {
+ // Use Custom type - the plugin's loader should handle it
+ // 使用 Custom 类型 - 插件的加载器应该处理它
+ assetType = AssetType.Custom;
+ }
+ }
+
+ database.addAsset({
+ guid: asset.guid,
+ path: asset.path,
+ type: assetType,
+ name: asset.name,
+ size: asset.size,
+ hash: asset.hash || '',
+ dependencies: [],
+ labels: [],
+ tags: new Map(),
+ lastModified: asset.lastModified,
+ version: 1
+ });
+ }
+
+ console.log(`[EngineService] Asset sync complete`);
+ }
+
/**
* Setup asset path resolver for EngineRenderSystem.
* 为 EngineRenderSystem 设置资产路径解析器。
@@ -873,8 +978,12 @@ export class EngineService {
dispose(): void {
this.stop();
+ // Don't dispose the global assetManager, just clear the reference
+ // 不要 dispose 全局 assetManager,只是清除引用
if (this._assetManager) {
- this._assetManager.dispose();
+ // Clear the database to free memory when switching projects
+ // 切换项目时清空数据库以释放内存
+ this._assetManager.getDatabase().clear();
this._assetManager = null;
}
diff --git a/packages/editor-app/src/styles/ContentBrowser.css b/packages/editor-app/src/styles/ContentBrowser.css
index 7555af69..2b6e7d5c 100644
--- a/packages/editor-app/src/styles/ContentBrowser.css
+++ b/packages/editor-app/src/styles/ContentBrowser.css
@@ -143,6 +143,61 @@
font-size: 12px;
}
+/* ==================== Managed Directory Indicators ==================== */
+.folder-tree-item.managed-root .folder-tree-icon {
+ color: #4fc1ff;
+}
+
+.folder-tree-item.managed-root .managed-icon {
+ color: #4fc1ff;
+}
+
+.managed-badge {
+ font-size: 9px;
+ padding: 1px 4px;
+ background: #4fc1ff22;
+ color: #4fc1ff;
+ border-radius: 3px;
+ margin-left: 6px;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+/* Warning icons in context menu */
+.warning-icon {
+ color: #f59e0b !important;
+}
+
+.menu-item-with-warning {
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+}
+
+.warning-badge {
+ position: absolute;
+ bottom: -2px;
+ right: -4px;
+ color: #f59e0b;
+}
+
+/* Context menu disabled item for unmanaged warning */
+.context-menu-item.disabled .warning-icon {
+ color: #f59e0b !important;
+}
+
+/* ==================== Drag and Drop Styles ==================== */
+.folder-tree-item.drag-over {
+ background: #1976d2 !important;
+ outline: 2px solid #42a5f5;
+}
+
+.cb-asset-item.drag-over {
+ background: #1976d2 !important;
+ outline: 2px solid #42a5f5;
+ border-radius: 4px;
+}
+
/* ==================== Right Panel - Content Area ==================== */
.content-browser-right {
flex: 1;
diff --git a/packages/editor-core/src/Plugin/PluginManager.ts b/packages/editor-core/src/Plugin/PluginManager.ts
index afef63a5..9c0dcc26 100644
--- a/packages/editor-core/src/Plugin/PluginManager.ts
+++ b/packages/editor-core/src/Plugin/PluginManager.ts
@@ -1080,11 +1080,16 @@ export class PluginManager implements IService {
const wasEnabled = plugin.enabled;
const isDefaultEnabled = plugin.plugin.manifest.defaultEnabled;
- // 如果插件在配置中明确列出,按配置来
- // 如果插件不在配置中但 defaultEnabled=true,保持启用(新插件不应被旧配置禁用)
- // If plugin is explicitly in config, follow config
- // If plugin is not in config but defaultEnabled=true, keep enabled (new plugins should not be disabled by old config)
- const shouldBeEnabled = inConfig || (isDefaultEnabled && !enabledPlugins.some(p => p === id));
+ // 逻辑:
+ // 1. 如果插件在配置中明确列出,启用它
+ // 2. 如果插件不在配置中但 defaultEnabled=true,也启用它(新插件不应被旧配置禁用)
+ // 3. 只有在配置中明确不包含且 defaultEnabled=false 的插件才禁用
+ //
+ // Logic:
+ // 1. If plugin is explicitly in config, enable it
+ // 2. If plugin is not in config but defaultEnabled=true, also enable it (new plugins should not be disabled by old config)
+ // 3. Only disable plugins that are not in config AND have defaultEnabled=false
+ const shouldBeEnabled = inConfig || isDefaultEnabled;
if (shouldBeEnabled && !wasEnabled) {
toEnable.push(id);
diff --git a/packages/editor-core/src/Services/AssetRegistryService.ts b/packages/editor-core/src/Services/AssetRegistryService.ts
index db8c32be..2e79b750 100644
--- a/packages/editor-core/src/Services/AssetRegistryService.ts
+++ b/packages/editor-core/src/Services/AssetRegistryService.ts
@@ -125,8 +125,17 @@ const EXTENSION_TYPE_MAP: Record = {
'.prefab': 'prefab',
'.tmx': 'tilemap',
'.tsx': 'tileset',
+ // Particle system
+ '.particle': 'particle',
};
+/**
+ * Directories managed by asset registry (GUID system)
+ * 被资产注册表(GUID 系统)管理的目录
+ */
+export const MANAGED_ASSET_DIRECTORIES = ['assets', 'scripts', 'scenes'] as const;
+export type ManagedAssetDirectory = typeof MANAGED_ASSET_DIRECTORIES[number];
+
// 使用从 IFileSystem.ts 导入的标准接口
// Using standard interface imported from IFileSystem.ts
@@ -482,13 +491,12 @@ export class AssetRegistryService {
const sep = this._projectPath.includes('\\') ? '\\' : '/';
- // 扫描多个目录: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' }
- ];
+ // 扫描多个目录:assets, scripts, scenes, ecs-scenes
+ // Scan multiple directories: assets, scripts, scenes, ecs-scenes
+ const directoriesToScan = MANAGED_ASSET_DIRECTORIES.map(name => ({
+ path: `${this._projectPath}${sep}${name}`,
+ name
+ }));
for (const dir of directoriesToScan) {
try {
@@ -789,6 +797,74 @@ export class AssetRegistryService {
return this._projectPath;
}
+ /**
+ * Get managed asset directories
+ * 获取被管理的资产目录
+ */
+ getManagedDirectories(): readonly string[] {
+ return MANAGED_ASSET_DIRECTORIES;
+ }
+
+ /**
+ * Check if a path is within a managed directory
+ * 检查路径是否在被管理的目录中
+ *
+ * @param pathToCheck - Absolute or relative path | 绝对或相对路径
+ * @returns Whether the path is in a managed directory | 路径是否在被管理的目录中
+ */
+ isPathManaged(pathToCheck: string): boolean {
+ if (!pathToCheck) return false;
+
+ // Normalize path
+ const normalizedPath = pathToCheck.replace(/\\/g, '/');
+
+ // Check if path starts with any managed directory
+ for (const dir of MANAGED_ASSET_DIRECTORIES) {
+ // Check relative path (e.g., "assets/textures/...")
+ if (normalizedPath.startsWith(`${dir}/`) || normalizedPath === dir) {
+ return true;
+ }
+ // Check absolute path (e.g., "C:/project/assets/...")
+ if (this._projectPath) {
+ const normalizedProject = this._projectPath.replace(/\\/g, '/');
+ const managedAbsPath = `${normalizedProject}/${dir}`;
+ if (normalizedPath.startsWith(`${managedAbsPath}/`) || normalizedPath === managedAbsPath) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the managed directory name for a path (if any)
+ * 获取路径所属的被管理目录名称(如果有)
+ *
+ * @param pathToCheck - Absolute or relative path | 绝对或相对路径
+ * @returns The managed directory name or null | 被管理的目录名称或 null
+ */
+ getManagedDirectoryForPath(pathToCheck: string): ManagedAssetDirectory | null {
+ if (!pathToCheck) return null;
+
+ const normalizedPath = pathToCheck.replace(/\\/g, '/');
+
+ for (const dir of MANAGED_ASSET_DIRECTORIES) {
+ if (normalizedPath.startsWith(`${dir}/`) || normalizedPath === dir) {
+ return dir;
+ }
+ if (this._projectPath) {
+ const normalizedProject = this._projectPath.replace(/\\/g, '/');
+ const managedAbsPath = `${normalizedProject}/${dir}`;
+ if (normalizedPath.startsWith(`${managedAbsPath}/`) || normalizedPath === managedAbsPath) {
+ return dir;
+ }
+ }
+ }
+
+ return null;
+ }
+
/**
* Dispose the service
*/