"""Project layout and schema helpers for MetaCore project v2.""" from __future__ import annotations import os import uuid from dataclasses import dataclass ENGINE_NAME = "MetaCore" PROJECT_SCHEMA_VERSION = 2 ASSET_DB_SCHEMA_VERSION = 1 METASCENE_SCHEMA_VERSION = 2 RUNTIME_SCENE_SCHEMA_VERSION = 2 MANIFEST_SCHEMA_VERSION = 1 SCENE_DESCRIPTION_EXTENSION = ".metascene" MODEL_EXTENSIONS = { ".bam", ".egg", ".obj", ".fbx", ".gltf", ".glb", ".ply", ".stl", } TEXTURE_EXTENSIONS = { ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".tif", ".tiff", ".dds", ".hdr", ".exr", } AUDIO_EXTENSIONS = {".wav", ".mp3", ".ogg", ".flac"} VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".wmv", ".webm"} SCRIPT_EXTENSIONS = {".py", ".pyc"} UI_EXTENSIONS = {".json", ".ttf", ".otf", ".woff", ".woff2"} def generate_guid() -> str: return uuid.uuid4().hex def normalize_path(path: str) -> str: return os.path.normpath(os.path.abspath(path)) def relative_project_path(project_root: str, target_path: str) -> str: project_root = normalize_path(project_root) target_path = normalize_path(target_path) try: relative_path = os.path.relpath(target_path, project_root) except ValueError: return "" if relative_path.startswith(".."): return "" return relative_path.replace("\\", "/") def detect_asset_type(file_path: str) -> str: extension = os.path.splitext(str(file_path or ""))[1].lower() if extension in MODEL_EXTENSIONS: return "model" if extension in TEXTURE_EXTENSIONS: return "texture" if extension in AUDIO_EXTENSIONS: return "audio" if extension in VIDEO_EXTENSIONS: return "video" if extension in SCRIPT_EXTENSIONS: return "script" if extension in UI_EXTENSIONS: return "ui" return "other" def get_asset_subdir_for_type(asset_type: str) -> str: return { "model": "Models", "texture": "Textures", "audio": "Audio", "video": "Video", "ui": "UI", "script": "Scripts", "other": "Misc", }.get(asset_type, "Misc") def default_build_settings() -> dict: return { "enabled_scene_guids": [], "output_format": "directory", "windows_player": True, "active_profile": "default", "profiles": { "default": { "name": "Default", "target_platform": "windows", "output_format": "directory", "output_dir": "Builds", "windows": { "enabled": True, "exe_name": "", "windowed": True, }, "runtime": { "startup_scene_guid": "", "enabled_scene_guids": [], "script_mode": "pyc", }, } }, } @dataclass(frozen=True) class ProjectLayout: project_root: str def __post_init__(self): object.__setattr__(self, "project_root", normalize_path(self.project_root)) @property def project_file(self) -> str: return os.path.join(self.project_root, "project.json") @property def assets_root(self) -> str: return os.path.join(self.project_root, "Assets") @property def scenes_root(self) -> str: return os.path.join(self.project_root, "Scenes") @property def library_root(self) -> str: return os.path.join(self.project_root, "Library") @property def builds_root(self) -> str: return os.path.join(self.project_root, "Builds") @property def asset_db_path(self) -> str: return os.path.join(self.library_root, "AssetDB.json") @property def imported_root(self) -> str: return os.path.join(self.library_root, "Imported") @property def build_cache_root(self) -> str: return os.path.join(self.library_root, "BuildCache") @property def scripts_root(self) -> str: return os.path.join(self.assets_root, "Scripts") @property def models_root(self) -> str: return os.path.join(self.assets_root, "Models") @property def textures_root(self) -> str: return os.path.join(self.assets_root, "Textures") @property def audio_root(self) -> str: return os.path.join(self.assets_root, "Audio") @property def video_root(self) -> str: return os.path.join(self.assets_root, "Video") @property def ui_root(self) -> str: return os.path.join(self.assets_root, "UI") @property def misc_root(self) -> str: return os.path.join(self.assets_root, "Misc") def meta_path_for_asset(self, asset_abs_path: str) -> str: return f"{normalize_path(asset_abs_path)}.meta" def imported_asset_dir(self, asset_guid: str) -> str: return os.path.join(self.imported_root, asset_guid) def build_cache_dir(self, scene_guid: str) -> str: return os.path.join(self.build_cache_root, scene_guid) def scene_file(self, scene_name: str) -> str: return os.path.join(self.scenes_root, f"{scene_name}{SCENE_DESCRIPTION_EXTENSION}") def ensure_project_directories(layout: ProjectLayout) -> None: for path in ( layout.assets_root, layout.models_root, layout.textures_root, layout.audio_root, layout.video_root, layout.ui_root, layout.scripts_root, layout.misc_root, layout.scenes_root, layout.library_root, layout.imported_root, layout.build_cache_root, layout.builds_root, ): os.makedirs(path, exist_ok=True)