Skip to main content
Glama
validation.ts8.64 kB
/** * Validation and sanitization utilities for Unreal Engine assets */ import { toRotTuple, toVec3Tuple } from './normalize.js'; /** * Maximum path length allowed in Unreal Engine */ const MAX_PATH_LENGTH = 260; /** * Invalid characters for Unreal Engine asset names * Note: Dashes are allowed in Unreal asset names */ // eslint-disable-next-line no-useless-escape const INVALID_CHARS = /[@#%$&*()+=\[\]{}<>?|\\;:'"`,~!\s]/g; /** * Reserved keywords that shouldn't be used as names */ const RESERVED_KEYWORDS = [ 'None', 'null', 'undefined', 'true', 'false', 'class', 'struct', 'enum', 'interface', 'default', 'transient', 'native' ]; /** * Sanitize an asset name for Unreal Engine * @param name The name to sanitize * @returns Sanitized name */ export function sanitizeAssetName(name: string): string { if (!name || typeof name !== 'string') { return 'Asset'; } // Remove leading/trailing whitespace let sanitized = name.trim(); // Replace invalid characters with underscores sanitized = sanitized.replace(INVALID_CHARS, '_'); // Remove consecutive underscores sanitized = sanitized.replace(/_+/g, '_'); // Remove leading/trailing underscores sanitized = sanitized.replace(/^_+|_+$/g, ''); // If name is empty after sanitization, use default if (!sanitized) { return 'Asset'; } // If name is a reserved keyword, append underscore if (RESERVED_KEYWORDS.includes(sanitized)) { sanitized = `${sanitized}_Asset`; } // Ensure name starts with a letter if (!/^[A-Za-z]/.test(sanitized)) { sanitized = `Asset_${sanitized}`; } // Truncate overly long names to reduce risk of hitting path length limits if (sanitized.length > 64) { sanitized = sanitized.slice(0, 64); } return sanitized; } /** * Sanitize a path for Unreal Engine * @param path The path to sanitize * @returns Sanitized path */ export function sanitizePath(path: string): string { if (!path || typeof path !== 'string') { return '/Game'; } // Normalize slashes path = path.replace(/\\/g, '/'); // Ensure path starts with / if (!path.startsWith('/')) { path = `/${path}`; } // Split path into segments and sanitize each let segments = path.split('/').filter(s => s.length > 0); if (segments.length === 0) { return '/Game'; } // Ensure the first segment is a valid root (Game, Engine, Script, Temp) const ROOTS = new Set(['Game', 'Engine', 'Script', 'Temp']); if (!ROOTS.has(segments[0])) { segments = ['Game', ...segments]; } const sanitizedSegments = segments.map(segment => { // Don't sanitize Game, Engine, or other root folders if (['Game', 'Engine', 'Script', 'Temp'].includes(segment)) { return segment; } return sanitizeAssetName(segment); }); // Reconstruct path const sanitizedPath = '/' + sanitizedSegments.join('/'); return sanitizedPath; } /** * Validate path length * @param path The full path to validate * @returns Object with validation result */ export function validatePathLength(path: string): { valid: boolean; error?: string } { if (path.length > MAX_PATH_LENGTH) { return { valid: false, error: `Path too long (${path.length} characters). Maximum allowed is ${MAX_PATH_LENGTH} characters.` }; } return { valid: true }; } /** * Validate and sanitize asset parameters * @param params Object containing name and optionally savePath * @returns Sanitized parameters with validation result */ export function validateAssetParams(params: { name: string; savePath?: string; [key: string]: any; }): { valid: boolean; sanitized: typeof params; error?: string; } { // Sanitize name const sanitizedName = sanitizeAssetName(params.name); // Sanitize path if provided const sanitizedPath = params.savePath ? sanitizePath(params.savePath) : params.savePath; // Construct full path for validation const fullPath = sanitizedPath ? `${sanitizedPath}/${sanitizedName}` : `/Game/${sanitizedName}`; // Validate path length const pathValidation = validatePathLength(fullPath); if (!pathValidation.valid) { return { valid: false, sanitized: params, error: pathValidation.error }; } return { valid: true, sanitized: { ...params, name: sanitizedName, ...(sanitizedPath && { savePath: sanitizedPath }) } }; } /** * Extract valid skeletal mesh path from various inputs * @param input The input path which might be a skeleton or mesh * @returns Corrected skeletal mesh path or null */ export function resolveSkeletalMeshPath(input: string): string | null { if (!input || typeof input !== 'string') { return null; } // Common skeleton to mesh mappings const skeletonToMeshMap: { [key: string]: string } = { '/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple', '/Game/Characters/Mannequins/Meshes/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple', '/Game/Mannequin/Character/Mesh/SK_Mannequin': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple', '/Game/Characters/Mannequin_UE4/Meshes/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple', '/Game/Characters/Mannequins/Skeletons/UE5_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple', '/Game/Characters/Mannequins/Skeletons/UE5_Female_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple', '/Game/Characters/Mannequins/Skeletons/UE5_Manny_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple', '/Game/Characters/Mannequins/Skeletons/UE5_Quinn_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple' }; // Check if this is a known skeleton path if (skeletonToMeshMap[input]) { return skeletonToMeshMap[input]; } // If it contains _Skeleton, try to convert to mesh name if (input.includes('_Skeleton')) { // Try common replacements let meshPath = input.replace('_Skeleton', ''); // Mapping for replacements const replacements: { [key: string]: string } = { '/SK_': '/SKM_', 'UE4_Mannequin': 'SKM_Manny', 'UE5_Mannequin': 'SKM_Manny', 'UE5_Manny': 'SKM_Manny', 'UE5_Quinn': 'SKM_Quinn' }; // Apply all replacements using regex meshPath = meshPath.replace( new RegExp(Object.keys(replacements).join('|'), 'g'), match => replacements[match] ); return meshPath; } // If it starts with SK_ (skeleton prefix), try SKM_ (skeletal mesh prefix) if (input.includes('/SK_')) { return input.replace('/SK_', '/SKM_'); } // Return as-is if no conversion needed return input; } /** * Concurrency delay to prevent race conditions * @param ms Milliseconds to delay */ export async function concurrencyDelay(ms: number = 100): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Ensure the provided value is a finite number within optional bounds. * @throws if the value is not a finite number or violates bounds */ export function validateNumber( value: unknown, label: string, { min, max, allowZero = true }: { min?: number; max?: number; allowZero?: boolean } = {} ): number { if (typeof value !== 'number' || !Number.isFinite(value)) { throw new Error(`Invalid ${label}: expected a finite number`); } if (!allowZero && value === 0) { throw new Error(`Invalid ${label}: zero is not allowed`); } if (typeof min === 'number' && value < min) { throw new Error(`Invalid ${label}: must be >= ${min}`); } if (typeof max === 'number' && value > max) { throw new Error(`Invalid ${label}: must be <= ${max}`); } return value; } /** * Validate an array (tuple) of finite numbers, preserving the original shape. * @throws if the tuple has the wrong length or contains invalid values */ export function ensureVector3(value: unknown, label: string): [number, number, number] { const tuple = toVec3Tuple(value); if (!tuple) { throw new Error(`Invalid ${label}: expected an object with x,y,z or an array of 3 numbers`); } return tuple; } export function ensureColorRGB(value: unknown, label: string): [number, number, number] { return ensureVector3(value, label); } export function ensureRotation(value: unknown, label: string): [number, number, number] { const tuple = toRotTuple(value); if (!tuple) { throw new Error(`Invalid ${label}: expected an object with pitch,yaw,roll or an array of 3 numbers`); } return tuple; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server